mirror of
https://github.com/nextcloud/server.git
synced 2026-06-14 19:20:35 -04:00
feat(OCM-invites): Add invitation class and emit event
I realize that we need to be able to react to accepted invites elsewhere, e.g. contacts app, so adding invite class and event for that purpose. Also rename the migration and bump the version so it will take affect correctly. Co-authored-by: Navid Shokri <navid.pdp11@gmail.com> Signed-off-by: Micke Nordin <kano@sunet.se>
This commit is contained in:
parent
bf31fa9dc7
commit
5999d77893
10 changed files with 111 additions and 53 deletions
|
|
@ -9,7 +9,7 @@
|
|||
<name>Cloud Federation API</name>
|
||||
<summary>Enable clouds to communicate with each other and exchange data</summary>
|
||||
<description>The Cloud Federation API enables various Nextcloud instances to communicate with each other and to exchange data.</description>
|
||||
<version>1.15.0</version>
|
||||
<version>1.16.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Bjoern Schiessle</author>
|
||||
<namespace>CloudFederationAPI</namespace>
|
||||
|
|
|
|||
|
|
@ -32,11 +32,6 @@ class InstalledVersions
|
|||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
|
|
@ -314,12 +309,6 @@ class InstalledVersions
|
|||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -333,27 +322,19 @@ class InstalledVersions
|
|||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = strtr(__DIR__, '\\', '/');
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -369,7 +350,7 @@ class InstalledVersions
|
|||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,5 +11,8 @@ return array(
|
|||
'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
|
||||
'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php',
|
||||
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php',
|
||||
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php',
|
||||
'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
|
||||
'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => $baseDir . '/../lib/Events/OCMInvitationAcceptedEvent.php',
|
||||
'OCA\\CloudFederationApi\\OCMInvitation' => $baseDir . '/../lib/OCMInvitation.php',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,10 @@ class ComposerStaticInitCloudFederationAPI
|
|||
'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
|
||||
'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php',
|
||||
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php',
|
||||
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php',
|
||||
'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
|
||||
'OCA\\CloudFederationApi\\Events\\OCMInvitationAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/OCMInvitationAcceptedEvent.php',
|
||||
'OCA\\CloudFederationApi\\OCMInvitation' => __DIR__ . '/..' . '/../lib/OCMInvitation.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\CloudFederationAPI\Controller;
|
||||
|
||||
use DateTime;
|
||||
|
|
@ -16,6 +18,8 @@ use NCU\Security\Signature\IIncomingSignedRequest;
|
|||
use NCU\Security\Signature\ISignatureManager;
|
||||
use OC\OCM\OCMSignatoryManager;
|
||||
use OCA\CloudFederationAPI\Config;
|
||||
use OCA\CloudFederationAPI\Events\OCMInvitationAcceptedEvent;
|
||||
use OCA\CloudFederationAPI\OCMInvitation;
|
||||
use OCA\CloudFederationAPI\ResponseDefinitions;
|
||||
use OCA\FederatedFileSharing\AddressHandler;
|
||||
use OCA\Federation\TrustedServers;
|
||||
|
|
@ -27,6 +31,7 @@ use OCP\AppFramework\Http\Attribute\OpenAPI;
|
|||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Federation\Exceptions\ActionNotSupportedException;
|
||||
use OCP\Federation\Exceptions\AuthenticationFailedException;
|
||||
use OCP\Federation\Exceptions\BadRequestException;
|
||||
|
|
@ -55,7 +60,8 @@ use Psr\Log\LoggerInterface;
|
|||
* @psalm-import-type CloudFederationAPIError from ResponseDefinitions
|
||||
*/
|
||||
#[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
|
||||
class RequestHandlerController extends Controller {
|
||||
class RequestHandlerController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
|
|
@ -65,6 +71,7 @@ class RequestHandlerController extends Controller {
|
|||
private IURLGenerator $urlGenerator,
|
||||
private ICloudFederationProviderManager $cloudFederationProviderManager,
|
||||
private Config $config,
|
||||
private IEventDispatcher $dispatcher,
|
||||
private IDBConnection $db,
|
||||
private readonly AddressHandler $addressHandler,
|
||||
private readonly IAppConfig $appConfig,
|
||||
|
|
@ -101,7 +108,8 @@ class RequestHandlerController extends Controller {
|
|||
#[PublicPage]
|
||||
#[NoCSRFRequired]
|
||||
#[BruteForceProtection(action: 'receiveFederatedShare')]
|
||||
public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) {
|
||||
public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType)
|
||||
{
|
||||
try {
|
||||
// if request is signed and well signed, no exception are thrown
|
||||
// if request is not signed and host is known for not supporting signed request, no exception are thrown
|
||||
|
|
@ -113,7 +121,8 @@ class RequestHandlerController extends Controller {
|
|||
}
|
||||
|
||||
// check if all required parameters are set
|
||||
if ($shareWith === null ||
|
||||
if (
|
||||
$shareWith === null ||
|
||||
$name === null ||
|
||||
$providerId === null ||
|
||||
$resourceType === null ||
|
||||
|
|
@ -189,7 +198,7 @@ class RequestHandlerController extends Controller {
|
|||
$share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType);
|
||||
$share->setProtocol($protocol);
|
||||
$provider->shareReceived($share);
|
||||
} catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) {
|
||||
} catch (ProviderDoesNotExistsException | ProviderCouldNotAddShareException $e) {
|
||||
return new JSONResponse(
|
||||
['message' => $e->getMessage()],
|
||||
Http::STATUS_NOT_IMPLEMENTED
|
||||
|
|
@ -243,7 +252,8 @@ class RequestHandlerController extends Controller {
|
|||
#[PublicPage]
|
||||
#[NoCSRFRequired]
|
||||
#[BruteForceProtection(action: 'inviteAccepted')]
|
||||
public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse {
|
||||
public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse
|
||||
{
|
||||
$this->logger->debug('Invite accepted for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name);
|
||||
|
||||
/** @var IQueryBuilder $qb */
|
||||
|
|
@ -255,6 +265,7 @@ class RequestHandlerController extends Controller {
|
|||
$data = $result->fetch();
|
||||
$result->closeCursor();
|
||||
$found_for_this_user = false;
|
||||
$updated = new DateTime("now");
|
||||
if ($data) {
|
||||
$found_for_this_user = $data['recipient_user_id'] === $userId && isset($data['user_id']);
|
||||
}
|
||||
|
|
@ -265,17 +276,22 @@ class RequestHandlerController extends Controller {
|
|||
$response->throttle();
|
||||
return $response;
|
||||
}
|
||||
if(!$this->trustedServers->isTrustedServer($recipientProvider)) {
|
||||
if (!$this->trustedServers->isTrustedServer($recipientProvider)) {
|
||||
$response = ['message' => 'Remote server not trusted', 'error' => true];
|
||||
$status = Http::STATUS_FORBIDDEN;
|
||||
return new JSONResponse($response,$status);
|
||||
return new JSONResponse($response, $status);
|
||||
}
|
||||
// Note: Not implementing 404 Invitation token does not exist, instead using 400
|
||||
|
||||
if ($data['accepted'] === true ) {
|
||||
if ($data['accepted'] === true) {
|
||||
$response = ['message' => 'Invite already accepted', 'error' => true];
|
||||
$status = Http::STATUS_CONFLICT;
|
||||
return new JSONResponse($response,$status);
|
||||
return new JSONResponse($response, $status);
|
||||
}
|
||||
if ($data['expiresAt'] < $updated) {
|
||||
$response = ['message' => 'Invitation expired', 'error' => true];
|
||||
$status = Http::STATUS_BAD_REQUEST;
|
||||
return new JSONResponse($response, $status);
|
||||
}
|
||||
|
||||
$localUser = $this->userManager->get($data['user_id']);
|
||||
|
|
@ -284,7 +300,6 @@ class RequestHandlerController extends Controller {
|
|||
|
||||
$response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName];
|
||||
$status = Http::STATUS_OK;
|
||||
$updated = new DateTime("now");
|
||||
$qb->update('federated_invites')
|
||||
->set('accepted', $qb->createNamedParameter(true))
|
||||
->set('acceptedAt', $qb->createNamedParameter($updated))
|
||||
|
|
@ -295,7 +310,21 @@ class RequestHandlerController extends Controller {
|
|||
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
|
||||
$qb->executeStatement();
|
||||
|
||||
return new JSONResponse($response,$status);
|
||||
$invitation = new OCMInvitation(
|
||||
accepted: true,
|
||||
recipient_email: $email,
|
||||
recipient_name: $name,
|
||||
recipient_user_id: $userId,
|
||||
recipient_provider: $recipientProvider,
|
||||
token: $token,
|
||||
user_id: $data['user_id'],
|
||||
acceptedAt: $updated,
|
||||
createdAt: $data['createdAt'],
|
||||
expiresAt: $data['expiresAt']
|
||||
);
|
||||
$this->dispatcher->dispatchTyped(new OCMInvitationAcceptedEvent($invitation));
|
||||
|
||||
return new JSONResponse($response, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -316,9 +345,11 @@ class RequestHandlerController extends Controller {
|
|||
#[NoCSRFRequired]
|
||||
#[PublicPage]
|
||||
#[BruteForceProtection(action: 'receiveFederatedShareNotification')]
|
||||
public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) {
|
||||
public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification)
|
||||
{
|
||||
// check if all required parameters are set
|
||||
if ($notificationType === null ||
|
||||
if (
|
||||
$notificationType === null ||
|
||||
$resourceType === null ||
|
||||
$providerId === null ||
|
||||
!is_array($notification)
|
||||
|
|
@ -394,7 +425,8 @@ class RequestHandlerController extends Controller {
|
|||
* @param string $uid
|
||||
* @return string mixed
|
||||
*/
|
||||
private function mapUid($uid) {
|
||||
private function mapUid($uid)
|
||||
{
|
||||
// FIXME this should be a method in the user management instead
|
||||
$this->logger->debug('shareWith before, ' . $uid, ['app' => $this->appName]);
|
||||
Util::emitHook(
|
||||
|
|
@ -417,12 +449,13 @@ class RequestHandlerController extends Controller {
|
|||
* @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request
|
||||
* @throws IncomingRequestException
|
||||
*/
|
||||
private function getSignedRequest(): ?IIncomingSignedRequest {
|
||||
private function getSignedRequest(): ?IIncomingSignedRequest
|
||||
{
|
||||
try {
|
||||
$signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager);
|
||||
$this->logger->debug('signed request available', ['signedRequest' => $signedRequest]);
|
||||
return $signedRequest;
|
||||
} catch (SignatureNotFoundException|SignatoryNotFoundException $e) {
|
||||
} catch (SignatureNotFoundException | SignatoryNotFoundException $e) {
|
||||
$this->logger->debug('remote does not support signed request', ['exception' => $e]);
|
||||
// remote does not support signed request.
|
||||
// currently we still accept unsigned request until lazy appconfig
|
||||
|
|
@ -452,7 +485,8 @@ class RequestHandlerController extends Controller {
|
|||
*
|
||||
* @throws IncomingRequestException
|
||||
*/
|
||||
private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void {
|
||||
private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void
|
||||
{
|
||||
if ($signedRequest === null) {
|
||||
$instance = $this->getHostFromFederationId($value);
|
||||
try {
|
||||
|
|
@ -516,7 +550,8 @@ class RequestHandlerController extends Controller {
|
|||
* @return void
|
||||
* @throws IncomingRequestException
|
||||
*/
|
||||
private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void {
|
||||
private function confirmNotificationEntry(?IIncomingSignedRequest $signedRequest, string $entry): void
|
||||
{
|
||||
$instance = $this->getHostFromFederationId($entry);
|
||||
if ($signedRequest === null) {
|
||||
try {
|
||||
|
|
@ -535,7 +570,8 @@ class RequestHandlerController extends Controller {
|
|||
* @return string
|
||||
* @throws IncomingRequestException
|
||||
*/
|
||||
private function getHostFromFederationId(string $entry): string {
|
||||
private function getHostFromFederationId(string $entry): string
|
||||
{
|
||||
if (!str_contains($entry, '@')) {
|
||||
throw new IncomingRequestException('entry ' . $entry . ' does not contains @');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Micke Nordin <kano@sunet.se>
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
namespace OCA\CloudFederationApi\Events;
|
||||
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCA\CloudFederationApi\OCMInvitation;
|
||||
|
||||
class OCMInvitationAcceptedEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private OCMInvitation $invitation
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getInvitation(): OCMInvitation
|
||||
{
|
||||
return $this->invitation;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ use OCP\DB\Types;
|
|||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version1015Date202502262004 extends SimpleMigrationStep
|
||||
class Version1016Date202502262004 extends SimpleMigrationStep
|
||||
{
|
||||
|
||||
/**
|
||||
18
apps/cloud_federation_api/lib/OCMInvitation.php
Normal file
18
apps/cloud_federation_api/lib/OCMInvitation.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace OCA\CloudFederationApi;
|
||||
|
||||
class OCMInvitation {
|
||||
public bool $accepted;
|
||||
public string $recipient_email;
|
||||
public string $recipient_name;
|
||||
public string $recipient_provider;
|
||||
public string $recipient_user_id;
|
||||
public string $token;
|
||||
public string $user_id;
|
||||
public \Datetime $acceptedAt;
|
||||
public \Datetime $createdAt;
|
||||
public \Datetime $expiresAt;
|
||||
|
||||
}
|
||||
|
|
@ -10,6 +10,5 @@ return array(
|
|||
'OC\\' => array($baseDir . '/lib/private'),
|
||||
'OCP\\' => array($baseDir . '/lib/public'),
|
||||
'NCU\\' => array($baseDir . '/lib/unstable'),
|
||||
'Bamarni\\Composer\\Bin\\' => array($vendorDir . '/bamarni/composer-bin-plugin/src'),
|
||||
'' => array($baseDir . '/lib/private/legacy'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -21,10 +21,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
array (
|
||||
'NCU\\' => 4,
|
||||
),
|
||||
'B' =>
|
||||
array (
|
||||
'Bamarni\\Composer\\Bin\\' => 21,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
|
|
@ -44,10 +40,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
array (
|
||||
0 => __DIR__ . '/../../..' . '/lib/unstable',
|
||||
),
|
||||
'Bamarni\\Composer\\Bin\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $fallbackDirsPsr4 = array (
|
||||
|
|
|
|||
Loading…
Reference in a new issue