feat(OCM-invites): Add entity and mapper

This patch introduces an entity and a mapper for Invites

Signed-off-by: Micke Nordin <kano@sunet.se>
This commit is contained in:
Micke Nordin 2025-03-06 14:13:55 +01:00 committed by Micke Nordin
parent 5999d77893
commit a178acfa1c
12 changed files with 240 additions and 127 deletions

View file

@ -11,8 +11,9 @@ 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\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.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',
);

View file

@ -26,10 +26,11 @@ 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\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.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)

View file

@ -18,8 +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\Db\FederatedInviteMapper;
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\Federation\TrustedServers;
@ -41,7 +41,6 @@ use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IURLGenerator;
@ -60,8 +59,7 @@ 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,
@ -72,14 +70,14 @@ class RequestHandlerController extends Controller
private ICloudFederationProviderManager $cloudFederationProviderManager,
private Config $config,
private IEventDispatcher $dispatcher,
private IDBConnection $db,
private FederatedInviteMapper $federatedInviteMapper,
private readonly AddressHandler $addressHandler,
private readonly IAppConfig $appConfig,
private ICloudFederationFactory $factory,
private ICloudIdManager $cloudIdManager,
private readonly ISignatureManager $signatureManager,
private readonly OCMSignatoryManager $signatoryManager,
private TrustedServers $trustedServers
private TrustedServers $trustedServers,
) {
parent::__construct($appName, $request);
}
@ -108,8 +106,7 @@ 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
@ -198,7 +195,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,33 +240,26 @@ class RequestHandlerController extends Controller
* @param string $email
* @param string $name
* @return JSONResponse
* 200: invitation accepted
* 400: Invalid token
* 403: Invitation token does not exist
* 409: User is allready known by the OCM provider
* spec link: https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
* 200: invitation accepted
* 400: Invalid token
* 403: Invitation token does not exist
* 409: User is allready known by the OCM provider
* spec link: https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
*/
#[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);
$updated = new \DateTime('now');
$updated = $updated->getTimeStamp();
/** @var IQueryBuilder $qb */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('federated_invites')
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
$result = $qb->executeQuery();
$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']);
$invitation = null;
if($token) {
$invitation = $this->federatedInviteMapper->findByToken($token);
}
if (!$found_for_this_user) {
if (!$invitation) {
$response = ['message' => 'Invalid or non existing token', 'error' => true];
$status = Http::STATUS_BAD_REQUEST;
$response = new JSONResponse($response, $status);
@ -283,46 +273,51 @@ class RequestHandlerController extends Controller
}
// Note: Not implementing 404 Invitation token does not exist, instead using 400
if ($data['accepted'] === true) {
if ($invitation->getAccepted() === true) {
$response = ['message' => 'Invite already accepted', 'error' => true];
$status = Http::STATUS_CONFLICT;
return new JSONResponse($response, $status);
}
if ($data['expiresAt'] < $updated) {
$unixstart = \DateTime::createFromFormat('U', '1');
$expiredAt = \DateTime::createFromFormat('U', strval($invitation->getExpiredAt()));
if ($expiredAt == $unixstart) {
$invitation->setExpiredAt($updated);
}
if ($invitation->getExpiredAt() < $updated) {
$response = ['message' => 'Invitation expired', 'error' => true];
$status = Http::STATUS_BAD_REQUEST;
return new JSONResponse($response, $status);
}
$localUser = $this->userManager->get($data['user_id']);
$localUser = $this->userManager->get($invitation->getUserId());
$sharedFromEmail = $localUser->getPrimaryEMailAddress();
$sharedFromDisplayName = $localUser->getDisplayName();
$response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName];
$response = ['userID' => $invitation->getUserId(), 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName];
$status = Http::STATUS_OK;
$qb->update('federated_invites')
->set('accepted', $qb->createNamedParameter(true))
->set('acceptedAt', $qb->createNamedParameter($updated))
->set('recipient_email', $qb->createNamedParameter($email))
->set('recipient_name', $qb->createNamedParameter($name))
->set('recipient_user_id', $qb->createNamedParameter($userId))
->set('recipient_provider', $qb->createNamedParameter($recipientProvider))
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
$qb->executeStatement();
$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));
$updated_data = [
'accepted' => true,
'recipient_email' => $email,
'recipient_name' => $name,
'recipient_user_id' => $userId,
'recipient_provider' => $recipientProvider,
'token' => $token,
'user_id' => $invitation->getUserId(),
'acceptedAt' => $updated,
'createdAt' => $invitation->getCreatedAt(),
'expiredAt' => $invitation->getExpiredAt()
];
$invitation = $this->federatedInviteMapper->dataArrayToInvite($updated_data);
if (!$this->federatedInviteMapper->update($invitation)) {
$response = ['message' => 'Error updating invitation', 'error' => true];
$status = Http::STATUS_BAD_REQUEST;
$response = new JSONResponse($response, $status);
return $response;
}
$event = new FederatedInviteAcceptedEvent($invitation);
$this->dispatcher->dispatchTyped($event);
return new JSONResponse($response, $status);
}
@ -345,8 +340,7 @@ 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 ||
@ -425,8 +419,7 @@ 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(
@ -449,13 +442,12 @@ 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
@ -485,8 +477,7 @@ 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 {
@ -550,8 +541,7 @@ 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 {
@ -570,8 +560,7 @@ 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 @');
}

View file

@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\CloudFederationAPI\Db;
use OCP\AppFramework\Db\Entity;
use OCP\DB\Types;
/**
* @method ?bool getAccepted()
* @method void setAccepted(bool $accepted)
* @method ?int getAcceptedAt()
* @method void setAcceptedAt(int $acceptedAt)
* @method ?int getCreatedAt()
* @method void setCreatedAt(int $createdAt)
* @method ?int getExpiredAt()
* @method void setExpiredAt(int $expiredAt)
* @method ?string getRecipientEmail()
* @method void setRecipientEmail(string $recipientEmail)
* @method ?string getRecipientName()
* @method void setRecipientName(string $recipientName)
* @method ?string getRecipientProvider()
* @method void setRecipientProvider(string $recipientProvider)
* @method ?string getRecipientUserId()
* @method void setRecipientUserId(string $recipientUserId)
* @method ?string getToken()
* @method void setToken(string $token)
* @method ?string getUserId()
* @method void setUserId(string $userId)
*/
class FederatedInvite extends Entity {
/**
* @var ?bool $accepted
*/
protected $accepted;
/**
* @var ?int $acceptedAt
*/
protected $acceptedAt;
/**
* @var int $createdAt
*/
protected $createdAt;
/**
* @var $int $expiredAt
*/
protected $expiredAt;
/**
* @var ?string $recipientEmail
*/
protected $recipientEmail;
/**
* @var ?string $recipientName
*/
protected $recipientName;
/**
* @var ?string $recipientProvider
*/
protected $recipientProvider;
/**
* @var ?string $recipientUserId
*/
protected $recipientUserId;
/**
* @var string $token
*/
protected $token;
/**
* @var string $userId
*/
protected $userId;
public function __construct() {
$this->addType('accepted', Types::BOOLEAN);
$this->addType('acceptedAt', Types::BIGINT);
$this->addType('createdAt', Types::BIGINT);
$this->addType('expiredAt', Types::BIGINT);
$this->addType('recipientEmail', Types::STRING);
$this->addType('recipientName', Types::STRING);
$this->addType('recipientProvider', Types::STRING);
$this->addType('recipientUserId', Types::STRING);
$this->addType('token', Types::STRING);
$this->addType('userId', Types::STRING);
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\CloudFederationAPI\Db;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<FederatedInvite>
*/
class FederatedInviteMapper extends QBMapper {
public const TABLE_NAME = 'federated_invites';
public function __construct(IDBConnection $db) {
parent::__construct($db, self::TABLE_NAME);
}
public function findByToken(string $token): ?FederatedInvite {
/** @var IQueryBuilder $qb */
if (!$token) {
return null;
}
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('federated_invites')
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
return $this->findEntity($qb);
}
public function dataArrayToInvite(array $data): FederatedInvite {
return $this->mapRowToEntity($data);
}
}

View file

@ -0,0 +1,24 @@
<?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 OCA\CloudFederationApi\Db\FederatedInvite;
use OCP\EventDispatcher\Event;
class FederatedInviteAcceptedEvent extends Event {
public function __construct(
private FederatedInvite $invitation,
) {
parent::__construct();
}
public function getInvitation(): FederatedInvite {
return $this->invitation;
}
}

View file

@ -1,26 +0,0 @@
<?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;
}
}

View file

@ -15,8 +15,7 @@ use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version1016Date202502262004 extends SimpleMigrationStep
{
class Version1016Date202502262004 extends SimpleMigrationStep {
/**
* @param IOutput $output
@ -24,8 +23,7 @@ class Version1016Date202502262004 extends SimpleMigrationStep
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
{
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
@ -75,22 +73,24 @@ class Version1016Date202502262004 extends SimpleMigrationStep
'notnull' => false,
'default' => false
]);
$table->addColumn('createdAt', Types::DATETIME, [
$table->addColumn('created_at', Types::BIGINT, [
'notnull' => true,
]);
$table->addColumn('expiredAt', Types::DATETIME, [
$table->addColumn('expired_at', Types::BIGINT, [
'notnull' => false,
]);
$table->addColumn('acceptedAt', Types::DATETIME, [
$table->addColumn('accepted_at', Types::BIGINT, [
'notnull' => false,
]);
$table->setPrimaryKey(['id']);
return $schema;
}
return $schema;
return null;
}
}

View file

@ -1,18 +0,0 @@
<?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;
}

View file

@ -10,5 +10,6 @@ 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'),
);

View file

@ -21,6 +21,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
'NCU\\' => 4,
),
'B' =>
array (
'Bamarni\\Composer\\Bin\\' => 21,
),
);
public static $prefixDirsPsr4 = array (
@ -40,6 +44,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
array (
0 => __DIR__ . '/../../..' . '/lib/unstable',
),
'Bamarni\\Composer\\Bin\\' =>
array (
0 => __DIR__ . '/..' . '/bamarni/composer-bin-plugin/src',
),
);
public static $fallbackDirsPsr4 = array (

View file

@ -58,7 +58,7 @@ interface IOCMProvider extends JsonSerializable {
* returns the capabilities of the API
*
* @return array
* @since 30.0.0
* @since 32.0.0
*/
public function getCapabilities(): array;
@ -68,7 +68,7 @@ interface IOCMProvider extends JsonSerializable {
* @param array $capabilities
*
* @return $this
* @since 30.0.0
* @since 32.0.0
*/
public function setCapabilities(array $capabilities): static;
@ -95,7 +95,7 @@ interface IOCMProvider extends JsonSerializable {
* get provider
*
* @return string
* @since 30.0.0
* @since 32.0.0
*/
public function getProvider(): string;
/**