mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 17:52:02 -04:00
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>
This commit is contained in:
parent
9e09cddf9b
commit
668246d380
7 changed files with 210 additions and 28 deletions
|
|
@ -20,11 +20,11 @@ return [
|
|||
'verb' => 'POST',
|
||||
'root' => '/ocm',
|
||||
],
|
||||
// [
|
||||
// 'name' => 'RequestHandler#inviteAccepted',
|
||||
// 'url' => '/invite-accepted',
|
||||
// 'verb' => 'POST',
|
||||
// 'root' => '/ocm',
|
||||
// ]
|
||||
[
|
||||
'name' => 'RequestHandler#inviteAccepted',
|
||||
'url' => '/invite-accepted',
|
||||
'verb' => 'POST',
|
||||
'root' => '/ocm',
|
||||
]
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ class InstalledVersions
|
|||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
|
|
@ -309,6 +314,12 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -322,19 +333,27 @@ 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';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -350,7 +369,7 @@ class InstalledVersions
|
|||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use OCP\OCM\IOCMProvider;
|
|||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class Capabilities implements ICapability {
|
||||
public const API_VERSION = '1.1'; // informative, real version.
|
||||
public const API_VERSION = '1.1.0';
|
||||
|
||||
public function __construct(
|
||||
private IURLGenerator $urlGenerator,
|
||||
|
|
@ -42,13 +42,16 @@ class Capabilities implements ICapability {
|
|||
* keyId: string,
|
||||
* publicKeyPem: string,
|
||||
* },
|
||||
* provider: string,
|
||||
* resourceTypes: list<array{
|
||||
* name: string,
|
||||
* shareTypes: list<string>,
|
||||
* protocols: array<string, string>
|
||||
* }>,
|
||||
* version: string
|
||||
* }
|
||||
* capabilities: array{
|
||||
* string,
|
||||
* }
|
||||
* }
|
||||
* @throws OCMArgumentException
|
||||
*/
|
||||
|
|
@ -57,6 +60,7 @@ class Capabilities implements ICapability {
|
|||
|
||||
$this->provider->setEnabled(true);
|
||||
$this->provider->setApiVersion(self::API_VERSION);
|
||||
$this->provider->setCapabilities(['/invite-accepted', '/notifications', '/shares']);
|
||||
|
||||
$pos = strrpos($url, '/');
|
||||
if ($pos === false) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
namespace OCA\CloudFederationAPI\Controller;
|
||||
|
||||
use DateTime;
|
||||
use NCU\Federation\ISignedCloudFederationProvider;
|
||||
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
|
||||
use NCU\Security\Signature\Exceptions\IncomingRequestException;
|
||||
|
|
@ -17,6 +18,7 @@ use OC\OCM\OCMSignatoryManager;
|
|||
use OCA\CloudFederationAPI\Config;
|
||||
use OCA\CloudFederationAPI\ResponseDefinitions;
|
||||
use OCA\FederatedFileSharing\AddressHandler;
|
||||
use OCA\Federation\TrustedServers;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
|
||||
|
|
@ -24,6 +26,7 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
|||
use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\Federation\Exceptions\ActionNotSupportedException;
|
||||
use OCP\Federation\Exceptions\AuthenticationFailedException;
|
||||
use OCP\Federation\Exceptions\BadRequestException;
|
||||
|
|
@ -33,6 +36,7 @@ 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;
|
||||
|
|
@ -61,12 +65,14 @@ class RequestHandlerController extends Controller {
|
|||
private IURLGenerator $urlGenerator,
|
||||
private ICloudFederationProviderManager $cloudFederationProviderManager,
|
||||
private Config $config,
|
||||
private IDBConnection $db,
|
||||
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
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
|
@ -213,6 +219,84 @@ class RequestHandlerController extends Controller {
|
|||
return new JSONResponse($responseData, Http::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform the sender that an invitation was accepted to start sharing
|
||||
*
|
||||
* Inform about an accepted invitation so the user on the sender provider's side
|
||||
* can initiate the OCM share creation. To protect the identity of the parties,
|
||||
* for shares created following an OCM invitation, the user id MAY be hashed,
|
||||
* and recipients implementing the OCM invitation workflow MAY refuse to process
|
||||
* shares coming from unknown parties.
|
||||
*
|
||||
* @param string $recipientProvider
|
||||
* @param string $token
|
||||
* @param string $userId
|
||||
* @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
|
||||
*/
|
||||
#[PublicPage]
|
||||
#[NoCSRFRequired]
|
||||
#[BruteForceProtection(action: 'inviteAccepted')]
|
||||
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 */
|
||||
$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;
|
||||
if ($data) {
|
||||
$found_for_this_user = $data['recipient_user_id'] === $userId && isset($data['user_id']);
|
||||
}
|
||||
if (!$found_for_this_user) {
|
||||
$response = ['message' => 'Invalid or non existing token', 'error' => true];
|
||||
$status = Http::STATUS_BAD_REQUEST;
|
||||
return new JSONResponse($response,$status);
|
||||
}
|
||||
if(!$this->trustedServers->isTrustedServer($recipientProvider)) {
|
||||
$response = ['message' => 'Remote server not trusted', 'error' => true];
|
||||
$status = Http::STATUS_FORBIDDEN;
|
||||
return new JSONResponse($response,$status);
|
||||
}
|
||||
// Note: Not implementing 404 Invitation token does not exist, instead using 400
|
||||
|
||||
if ($data['accepted'] === true ) {
|
||||
$response = ['message' => 'Invite already accepted', 'error' => true];
|
||||
$status = Http::STATUS_CONFLICT;
|
||||
return new JSONResponse($response,$status);
|
||||
}
|
||||
|
||||
$localUser = $this->userManager->get($data['user_id']);
|
||||
$sharedFromEmail = $localUser->getPrimaryEMailAddress();
|
||||
$sharedFromDisplayName = $localUser->getDisplayName();
|
||||
|
||||
$response = ['userID' => $data['user_id'], 'email' => $sharedFromEmail, 'name' => $sharedFromDisplayName];
|
||||
$status = Http::STATUS_OK;
|
||||
$updated = new DateTime("now");
|
||||
$qb->update('federated_invites f')
|
||||
->set('f.accepted', $qb->createNamedParameter(true))
|
||||
->set('f.acceptedAt', $qb->createNamedParameter($updated))
|
||||
->set('f.recipient_email', $qb->createNamedParameter($email))
|
||||
->set('f.recipient_name', $qb->createNamedParameter($name))
|
||||
->set('f.recipient_user_id', $qb->createNamedParameter($userId))
|
||||
->set('f.recipient_provider', $qb->createNamedParameter($recipientProvider))
|
||||
->where($qb->expr()->eq('token', $qb->createNamedParameter($token)));
|
||||
$result = $qb->executeQuery();
|
||||
$result->closeCursor();
|
||||
|
||||
return new JSONResponse($response,$status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification about an existing share
|
||||
*
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ namespace OCA\CloudFederationAPI\Migration;
|
|||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version0001Date202502262004 extends SimpleMigrationStep
|
||||
class Version1015Date202502262004 extends SimpleMigrationStep
|
||||
{
|
||||
|
||||
/**
|
||||
|
|
@ -34,42 +35,55 @@ class Version0001Date202502262004 extends SimpleMigrationStep
|
|||
if (! $schema->hasTable($table_name)) {
|
||||
|
||||
$table = $schema->createTable($table_name);
|
||||
$table->addColumn('id', 'bigint', [
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
'length' => 20,
|
||||
'length' => 11,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
|
||||
$table->addColumn('user_id', 'bigint', [
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 20,
|
||||
'unsigned' => true,
|
||||
'length' => 64,
|
||||
|
||||
]);
|
||||
|
||||
|
||||
$table->addColumn('token', 'string', [
|
||||
// https://saturncloud.io/blog/what-is-the-maximum-length-of-a-url-in-different-browsers/#maximum-url-length-in-different-browsers
|
||||
// We use the least common denominator, the minimum length supported by browsers
|
||||
$table->addColumn('recipient_provider', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 2083,
|
||||
]);
|
||||
$table->addColumn('recipient_user_id', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 1024,
|
||||
]);
|
||||
$table->addColumn('recipient_name', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 1024,
|
||||
]);
|
||||
// https://www.directedignorance.com/blog/maximum-length-of-email-address
|
||||
$table->addColumn('recipient_email', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 320,
|
||||
]);
|
||||
$table->addColumn('token', Types::STRING, [
|
||||
'notnull' => true,
|
||||
'length' => 60,
|
||||
]);
|
||||
$table->addColumn('email', 'string', [
|
||||
'notnull' => true,
|
||||
'length' => 256,
|
||||
]);
|
||||
$table->addColumn('accepted', 'boolean', [
|
||||
$table->addColumn('accepted', Types::BOOLEAN, [
|
||||
'notnull' => false,
|
||||
'default' => false
|
||||
]);
|
||||
$table->addColumn('createdAt', 'datetime', [
|
||||
$table->addColumn('createdAt', Types::DATETIME, [
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addColumn('expiredAt', 'datetime', [
|
||||
$table->addColumn('expiredAt', Types::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
$table->addColumn('acceptedAt', 'datetime', [
|
||||
$table->addColumn('acceptedAt', Types::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
|
||||
|
|
@ -16,13 +16,17 @@ use OCP\OCM\Exceptions\OCMArgumentException;
|
|||
use OCP\OCM\Exceptions\OCMProviderException;
|
||||
use OCP\OCM\IOCMProvider;
|
||||
use OCP\OCM\IOCMResource;
|
||||
use OCP\IConfig;
|
||||
|
||||
/**
|
||||
* @since 28.0.0
|
||||
*/
|
||||
class OCMProvider implements IOCMProvider {
|
||||
private IConfig $config;
|
||||
private string $provider;
|
||||
private bool $enabled = false;
|
||||
private string $apiVersion = '';
|
||||
private array $capabilities = [];
|
||||
private string $endPoint = '';
|
||||
/** @var IOCMResource[] */
|
||||
private array $resourceTypes = [];
|
||||
|
|
@ -31,7 +35,10 @@ class OCMProvider implements IOCMProvider {
|
|||
|
||||
public function __construct(
|
||||
protected IEventDispatcher $dispatcher,
|
||||
IConfig $config,
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->provider = 'Nextcloud ' . $config->getSystemValue('version');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +95,34 @@ class OCMProvider implements IOCMProvider {
|
|||
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 $key => $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
|
||||
|
|
|
|||
|
|
@ -54,6 +54,25 @@ interface IOCMProvider extends JsonSerializable {
|
|||
*/
|
||||
public function getApiVersion(): string;
|
||||
|
||||
/**
|
||||
* returns the capabilities of the API
|
||||
*
|
||||
* @return array
|
||||
* @since 30.0.0
|
||||
*/
|
||||
public function getCapabilities(): array;
|
||||
|
||||
/**
|
||||
* set the capabilities of the API
|
||||
*
|
||||
* @param array $capabilities
|
||||
*
|
||||
* @return $this
|
||||
* @since 30.0.0
|
||||
*/
|
||||
|
||||
public function setCapabilities(array $capabilities): static;
|
||||
|
||||
/**
|
||||
* configure endpoint
|
||||
*
|
||||
|
|
@ -72,6 +91,13 @@ interface IOCMProvider extends JsonSerializable {
|
|||
*/
|
||||
public function getEndPoint(): string;
|
||||
|
||||
/**
|
||||
* get provider
|
||||
*
|
||||
* @return string
|
||||
* @since 30.0.0
|
||||
*/
|
||||
public function getProvider(): string;
|
||||
/**
|
||||
* create a new resource to later add it with {@see addResourceType()}
|
||||
* @return IOCMResource
|
||||
|
|
|
|||
Loading…
Reference in a new issue