Skip to content

Commit 0c3885f

Browse files
committed
feat(OCM-invites): Implementation of invitation flow
This patchset implements the /invite-accepted endpoint https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post Also normalize names of columns, and populate them all Inspo from: - apps/dav/lib/Migration/Version1005Date20180413093149.php - https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers - https://www.directedignorance.com/blog/maximum-length-of-email-address Signed-off-by: Micke Nordin <kano@sunet.se>
1 parent aba22e5 commit 0c3885f

File tree

7 files changed

+212
-28
lines changed

7 files changed

+212
-28
lines changed

apps/cloud_federation_api/appinfo/routes.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
'verb' => 'POST',
2121
'root' => '/ocm',
2222
],
23-
// [
24-
// 'name' => 'RequestHandler#inviteAccepted',
25-
// 'url' => '/invite-accepted',
26-
// 'verb' => 'POST',
27-
// 'root' => '/ocm',
28-
// ]
23+
[
24+
'name' => 'RequestHandler#inviteAccepted',
25+
'url' => '/invite-accepted',
26+
'verb' => 'POST',
27+
'root' => '/ocm',
28+
]
2929
],
3030
];

apps/cloud_federation_api/composer/composer/InstalledVersions.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ class InstalledVersions
3232
*/
3333
private static $installed;
3434

35+
/**
36+
* @var bool
37+
*/
38+
private static $installedIsLocalDir;
39+
3540
/**
3641
* @var bool|null
3742
*/
@@ -309,6 +314,12 @@ public static function reload($data)
309314
{
310315
self::$installed = $data;
311316
self::$installedByVendor = array();
317+
318+
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
319+
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
320+
// so we have to assume it does not, and that may result in duplicate data being returned when listing
321+
// all installed packages for example
322+
self::$installedIsLocalDir = false;
312323
}
313324

314325
/**
@@ -322,19 +333,27 @@ private static function getInstalled()
322333
}
323334

324335
$installed = array();
336+
$copiedLocalDir = false;
325337

326338
if (self::$canGetVendors) {
339+
$selfDir = strtr(__DIR__, '\\', '/');
327340
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
341+
$vendorDir = strtr($vendorDir, '\\', '/');
328342
if (isset(self::$installedByVendor[$vendorDir])) {
329343
$installed[] = self::$installedByVendor[$vendorDir];
330344
} elseif (is_file($vendorDir.'/composer/installed.php')) {
331345
/** @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 */
332346
$required = require $vendorDir.'/composer/installed.php';
333-
$installed[] = self::$installedByVendor[$vendorDir] = $required;
334-
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
335-
self::$installed = $installed[count($installed) - 1];
347+
self::$installedByVendor[$vendorDir] = $required;
348+
$installed[] = $required;
349+
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
350+
self::$installed = $required;
351+
self::$installedIsLocalDir = true;
336352
}
337353
}
354+
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
355+
$copiedLocalDir = true;
356+
}
338357
}
339358
}
340359

@@ -350,7 +369,7 @@ private static function getInstalled()
350369
}
351370
}
352371

353-
if (self::$installed !== array()) {
372+
if (self::$installed !== array() && !$copiedLocalDir) {
354373
$installed[] = self::$installed;
355374
}
356375

apps/cloud_federation_api/lib/Capabilities.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use Psr\Log\LoggerInterface;
2020

2121
class Capabilities implements ICapability {
22-
public const API_VERSION = '1.1'; // informative, real version.
22+
public const API_VERSION = '1.1.0';
2323

2424
public function __construct(
2525
private IURLGenerator $urlGenerator,
@@ -42,13 +42,16 @@ public function __construct(
4242
* keyId: string,
4343
* publicKeyPem: string,
4444
* },
45+
* provider: string,
4546
* resourceTypes: list<array{
4647
* name: string,
4748
* shareTypes: list<string>,
4849
* protocols: array<string, string>
4950
* }>,
5051
* version: string
51-
* }
52+
* capabilities: array{
53+
* string,
54+
* }
5255
* }
5356
* @throws OCMArgumentException
5457
*/
@@ -57,6 +60,7 @@ public function getCapabilities() {
5760

5861
$this->provider->setEnabled(true);
5962
$this->provider->setApiVersion(self::API_VERSION);
63+
$this->provider->setCapabilities(['/invite-accepted', '/notifications', '/shares']);
6064

6165
$pos = strrpos($url, '/');
6266
if ($pos === false) {

apps/cloud_federation_api/lib/Controller/RequestHandlerController.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
namespace OCA\CloudFederationAPI\Controller;
77

8+
use DateTime;
89
use NCU\Federation\ISignedCloudFederationProvider;
910
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
1011
use NCU\Security\Signature\Exceptions\IncomingRequestException;
@@ -17,13 +18,15 @@
1718
use OCA\CloudFederationAPI\Config;
1819
use OCA\CloudFederationAPI\ResponseDefinitions;
1920
use OCA\FederatedFileSharing\AddressHandler;
21+
use OCA\Federation\TrustedServers;
2022
use OCP\AppFramework\Controller;
2123
use OCP\AppFramework\Http;
2224
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
2325
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
2426
use OCP\AppFramework\Http\Attribute\OpenAPI;
2527
use OCP\AppFramework\Http\Attribute\PublicPage;
2628
use OCP\AppFramework\Http\JSONResponse;
29+
use OCP\DB\QueryBuilder\IQueryBuilder;
2730
use OCP\Federation\Exceptions\ActionNotSupportedException;
2831
use OCP\Federation\Exceptions\AuthenticationFailedException;
2932
use OCP\Federation\Exceptions\BadRequestException;
@@ -33,6 +36,7 @@
3336
use OCP\Federation\ICloudFederationProviderManager;
3437
use OCP\Federation\ICloudIdManager;
3538
use OCP\IAppConfig;
39+
use OCP\IDBConnection;
3640
use OCP\IGroupManager;
3741
use OCP\IRequest;
3842
use OCP\IURLGenerator;
@@ -61,12 +65,14 @@ public function __construct(
6165
private IURLGenerator $urlGenerator,
6266
private ICloudFederationProviderManager $cloudFederationProviderManager,
6367
private Config $config,
68+
private IDBConnection $db,
6469
private readonly AddressHandler $addressHandler,
6570
private readonly IAppConfig $appConfig,
6671
private ICloudFederationFactory $factory,
6772
private ICloudIdManager $cloudIdManager,
6873
private readonly ISignatureManager $signatureManager,
6974
private readonly OCMSignatoryManager $signatoryManager,
75+
private TrustedServers $trustedServers
7076
) {
7177
parent::__construct($appName, $request);
7278
}
@@ -213,6 +219,84 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
213219
return new JSONResponse($responseData, Http::STATUS_CREATED);
214220
}
215221

222+
/**
223+
* Inform the sender that an invitation was accepted to start sharing
224+
*
225+
* Inform about an accepted invitation so the user on the sender provider's side
226+
* can initiate the OCM share creation. To protect the identity of the parties,
227+
* for shares created following an OCM invitation, the user id MAY be hashed,
228+
* and recipients implementing the OCM invitation workflow MAY refuse to process
229+
* shares coming from unknown parties.
230+
*
231+
* @param string $recipientProvider
232+
* @param string $token
233+
* @param string $userId
234+
* @param string $email
235+
* @param string $name
236+
* @return JSONResponse
237+
* 200: invitation accepted
238+
* 400: Invalid token
239+
* 403: Invitation token does not exist
240+
* 409: User is allready known by the OCM provider
241+
* spec link: https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
242+
*/
243+
#[PublicPage]
244+
#[NoCSRFRequired]
245+
#[BruteForceProtection(action: 'inviteAccepted')]
246+
public function inviteAccepted(string $recipientProvider, string $token, string $userId, string $email, string $name): JSONResponse {
247+
$this->logger->debug('Invite accepted for ' . $userId . ' with token ' . $token . ' and email ' . $email . ' and name ' . $name);
248+
249+
/** @var IQueryBuilder $qb */
250+
$qb = $this->db->getQueryBuilder();
251+
$qb->select('*')
252+
->from('federated_invites')
253+
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
254+
$result = $qb->executeQuery();
255+
$data = $result->fetch();
256+
$result->closeCursor();
257+
$found_for_this_user = false;
258+
if ($data) {
259+
$found_for_this_user = $data['recipient_user_id'] === $userId && isset($data['user_id']);
260+
}
261+
if (!$found_for_this_user) {
262+
$response = ['message' => 'Invalid or non existing token', 'error' => true];
263+
$status = Http::STATUS_BAD_REQUEST;
264+
return new JSONResponse($response,$status);
265+
}
266+
if(!$this->trustedServers->isTrustedServer($recipientProvider)) {
267+
$response = ['message' => 'Remote server not trusted', 'error' => true];
268+
$status = Http::STATUS_FORBIDDEN;
269+
return new JSONResponse($response,$status);
270+
}
271+
// Note: Not implementing 404 Invitation token does not exist, instead using 400
272+
273+
if ($data['accepted'] === true ) {
274+
$response = ['message' => 'Invite already accepted', 'error' => true];
275+
$status = Http::STATUS_CONFLICT;
276+
return new JSONResponse($response,$status);
277+
}
278+
279+
$localUser = $this->userManager->get($data['user_id']);
280+
$sharedFromEmail = $localUser->getPrimaryEMailAddress();
281+
$sharedFromDisplayName = $localUser->getDisplayName();
282+
283+
$response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName];
284+
$status = Http::STATUS_OK;
285+
$updated = new DateTime("now");
286+
$qb->update('federated_invites f')
287+
->set('f.accepted', $qb->createNamedParameter(true))
288+
->set('f.acceptedAt', $qb->createNamedParameter($updated))
289+
->set('f.recipient_email', $qb->createNamedParameter($email))
290+
->set('f.recipient_name', $qb->createNamedParameter($name))
291+
->set('f.recipient_user_id', $qb->createNamedParameter($userId))
292+
->set('f.recipient_provider', $qb->createNamedParameter($recipientProvider))
293+
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
294+
$result = $qb->executeQuery();
295+
$result->closeCursor();
296+
297+
return new JSONResponse($response,$status);
298+
}
299+
216300
/**
217301
* Send a notification about an existing share
218302
*

apps/cloud_federation_api/lib/Migration/Version0001Date202502262004.php renamed to apps/cloud_federation_api/lib/Migration/Version1015Date202502262004.php

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111

1212
use Closure;
1313
use OCP\DB\ISchemaWrapper;
14+
use OCP\DB\Types;
1415
use OCP\Migration\IOutput;
1516
use OCP\Migration\SimpleMigrationStep;
1617

17-
class Version0001Date202502262004 extends SimpleMigrationStep
18+
class Version1015Date202502262004 extends SimpleMigrationStep
1819
{
1920

2021
/**
@@ -34,42 +35,55 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
3435
if (! $schema->hasTable($table_name)) {
3536

3637
$table = $schema->createTable($table_name);
37-
$table->addColumn('id', 'bigint', [
38+
$table->addColumn('id', Types::BIGINT, [
3839
'autoincrement' => true,
3940
'notnull' => true,
40-
'length' => 20,
41+
'length' => 11,
4142
'unsigned' => true,
4243
]);
4344

44-
$table->addColumn('user_id', 'bigint', [
45+
$table->addColumn('user_id', Types::STRING, [
4546
'notnull' => false,
46-
'length' => 20,
47-
'unsigned' => true,
47+
'length' => 64,
4848

4949
]);
5050

51-
52-
$table->addColumn('token', 'string', [
51+
// https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers
52+
// We use the least common denominator, the minimum length supported by browsers
53+
$table->addColumn('recipient_provider', Types::STRING, [
5354
'notnull' => true,
54-
'length' => 60,
55+
'length' => 2083,
56+
]);
57+
$table->addColumn('recipient_user_id', Types::STRING, [
58+
'notnull' => true,
59+
'length' => 1024,
5560
]);
56-
$table->addColumn('email', 'string', [
61+
$table->addColumn('recipient_name', Types::STRING, [
5762
'notnull' => true,
58-
'length' => 256,
63+
'length' => 1024,
64+
]);
65+
// https://www.directedignorance.com/blog/maximum-length-of-email-address
66+
$table->addColumn('recipient_email', Types::STRING, [
67+
'notnull' => true,
68+
'length' => 320,
69+
]);
70+
$table->addColumn('token', Types::STRING, [
71+
'notnull' => true,
72+
'length' => 60,
5973
]);
60-
$table->addColumn('accepted', 'boolean', [
74+
$table->addColumn('accepted', Types::BOOLEAN, [
6175
'notnull' => false,
6276
'default' => false
6377
]);
64-
$table->addColumn('createdAt', 'datetime', [
78+
$table->addColumn('createdAt', Types::DATETIME, [
6579
'notnull' => true,
6680
]);
6781

68-
$table->addColumn('expiredAt', 'datetime', [
82+
$table->addColumn('expiredAt', Types::DATETIME, [
6983
'notnull' => false,
7084
]);
7185

72-
$table->addColumn('acceptedAt', 'datetime', [
86+
$table->addColumn('acceptedAt', Types::DATETIME, [
7387
'notnull' => false,
7488
]);
7589

0 commit comments

Comments
 (0)