Skip to content

Commit

Permalink
Merge pull request #1173 from nextcloud/feat/transfer-endpoint
Browse files Browse the repository at this point in the history
feat: Add transfer endpoint
  • Loading branch information
Pytal authored Jul 12, 2024
2 parents 07f19a6 + e352136 commit 51e2a57
Show file tree
Hide file tree
Showing 11 changed files with 571 additions and 223 deletions.
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
'url' => '/api/v1/users/{userId}',
'verb' => 'GET'
],
[
'name' => 'users#transfer',
'url' => '/api/v1/transfer',
'verb' => 'PUT'
],
[
'name' => 'API#languages',
'url' => '/api/v1/languages',
Expand Down
4 changes: 4 additions & 0 deletions img/account-arrow-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions lib/BackgroundJob/TransferJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Guests\BackgroundJob;

use OCA\Guests\AppInfo\Application;
use OCA\Guests\Db\Transfer;
use OCA\Guests\Db\TransferMapper;
use OCA\Guests\TransferService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager as NotificationManager;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;

class TransferJob extends QueuedJob {
public function __construct(
ITimeFactory $time,
private IUserManager $userManager,
private ISecureRandom $secureRandom,
private NotificationManager $notificationManager,
private IURLGenerator $urlGenerator,
private TransferService $transferService,
private TransferMapper $transferMapper,
private LoggerInterface $logger,
) {
parent::__construct($time);
}

private function notifyFailure(Transfer $transfer): void {
$notification = $this->notificationManager->createNotification();
$notification
->setApp(Application::APP_ID)
->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'account-arrow-right.svg')))
->setUser($transfer->getAuthor())
->setDateTime($this->time->getDateTime())
->setObject('guest-transfer', (string)$transfer->getId())
->setSubject('guest-transfer-fail', [
'source' => $transfer->getSource(),
'target' => $transfer->getTarget(),
]);
$this->notificationManager->notify($notification);
}

private function notifySuccess(Transfer $transfer): void {
$notification = $this->notificationManager->createNotification();
$notification
->setApp(Application::APP_ID)
->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath(Application::APP_ID, 'account-arrow-right.svg')))
->setUser($transfer->getAuthor())
->setDateTime($this->time->getDateTime())
->setObject('guest-transfer', (string)$transfer->getId())
->setSubject('guest-transfer-done', [
'source' => $transfer->getSource(),
'target' => $transfer->getTarget(),
]);
$this->notificationManager->notify($notification);
}

private function fail(Transfer $transfer, ?IUser $targetUser = null): void {
$this->notifyFailure($transfer);
$this->transferMapper->delete($transfer);
if (!($targetUser instanceof IUser)) {
return;
}
$result = $targetUser->delete(); // Rollback created user
if (!$result) {
$this->logger->error('Failed to delete target user', ['user' => $targetUser->getUID()]);
}
}

public function run($argument): void {
/** @var int $id */
$id = $argument['id'];

$transfer = $this->transferMapper->getById($id);
$transfer->setStatus(Transfer::STATUS_STARTED);
$this->transferMapper->update($transfer);

$source = $transfer->getSource();
$target = $transfer->getTarget();

$sourceUser = $this->userManager->get($source);
if (!($sourceUser instanceof IUser)) {
$this->logger->error('Failed to transfer missing guest user: ' . $source);
$this->fail($transfer);
return;
}

if ($this->userManager->userExists($target)) {
$this->logger->error("Cannot transfer guest user \"$source\", target user \"$target\" already exists");
$this->fail($transfer);
return;
}

$targetUser = $this->userManager->createUser(
$target,
$this->secureRandom->generate(20), // Password hash will be copied to target user from source user
);

if (!($targetUser instanceof IUser)) {
$this->logger->error('Failed to create new user: ' . $target);
$this->fail($transfer);
return;
}

$targetUser->setSystemEMailAddress($sourceUser->getUID()); // Guest user id is an email

try {
$this->transferService->transfer($sourceUser, $targetUser);
} catch (\Throwable $th) {
$this->logger->error($th->getMessage(), ['exception' => $th]);
$this->fail($transfer, $targetUser);
return;
}

$passwordHash = $sourceUser->getPasswordHash();
if (empty($passwordHash)) {
$this->logger->error('Invalid guest password hash', ['guest' => $sourceUser->getUID(), 'passwordHash' => $passwordHash]);
$this->fail($transfer, $targetUser);
return;
}

$setPasswordHashResult = $targetUser->setPasswordHash($passwordHash); // Copy password hash after transfer to prevent login before completion
if (!$setPasswordHashResult) {
$this->logger->error('Failed to set password hash on target user', ['user' => $targetUser->getUID()]);
$this->fail($transfer, $targetUser);
return;
}

$result = $sourceUser->delete();
if (!$result) {
$this->logger->error('Failed to delete guest user', ['user' => $sourceUser->getUID()]);
}
$this->notifySuccess($transfer);
$this->transferMapper->delete($transfer);
}
}
123 changes: 76 additions & 47 deletions lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
@@ -1,74 +1,49 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Guests\Controller;

use OC\Hooks\PublicEmitter;
use OCA\Guests\Config;
use OCA\Guests\Db\Transfer;
use OCA\Guests\Db\TransferMapper;
use OCA\Guests\GuestManager;
use OCA\Guests\TransferService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Mail\IMailer;

class UsersController extends OCSController {
/**
* @var IRequest
*/
protected $request;
/**
* @var IUserManager
*/
private $userManager;
/**
* @var IL10N
*/
private $l10n;
/**
* @var IMailer
*/
private $mailer;
/**
* @var GuestManager
*/
private $guestManager;
/** @var IUserSession */
private $userSession;
/** @var Config */
private $config;
/** @var ISubAdmin */
private $subAdmin;
/** @var IGroupManager */
private $groupManager;

public function __construct(
string $appName,
IRequest $request,
IUserManager $userManager,
IL10N $l10n,
Config $config,
IMailer $mailer,
GuestManager $guestManager,
IUserSession $userSession,
ISubAdmin $subAdmin,
IGroupManager $groupManager
private IUserManager $userManager,
private IL10N $l10n,
private Config $config,
private IMailer $mailer,
private GuestManager $guestManager,
private IUserSession $userSession,
private ISubAdmin $subAdmin,
private IGroupManager $groupManager,
private TransferService $transferService,
private TransferMapper $transferMapper,
) {
parent::__construct($appName, $request);

$this->request = $request;
$this->userManager = $userManager;
$this->l10n = $l10n;
$this->mailer = $mailer;
$this->guestManager = $guestManager;
$this->userSession = $userSession;
$this->config = $config;
$this->subAdmin = $subAdmin;
$this->groupManager = $groupManager;
}

/**
Expand Down Expand Up @@ -199,4 +174,58 @@ public function get(string $userId): DataResponse {

return new DataResponse($guests);
}

/**
* Transfer guest to a full account
*/
public function transfer(string $guestUserId, string $targetUserId): DataResponse {
$author = $this->userSession->getUser();
if (!($author instanceof IUser)) {
return new DataResponse([
'message' => $this->l10n->t('Failed to authorize')
], Http::STATUS_UNAUTHORIZED);
}

$sourceUser = $this->userManager->get($guestUserId);
if (!($sourceUser instanceof IUser)) {
return new DataResponse([
'message' => $this->l10n->t('Guest does not exist')
], Http::STATUS_NOT_FOUND);
}

if ($this->userManager->userExists($targetUserId)) {
return new DataResponse([
'message' => $this->l10n->t('User already exists')
], Http::STATUS_CONFLICT);
}

if (!$this->guestManager->isGuest($sourceUser)) {
return new DataResponse([
'message' => $this->l10n->t('User is not a guest'),
], Http::STATUS_CONFLICT);
}

try {
$transfer = $this->transferMapper->getBySource($sourceUser->getUID());
} catch (DoesNotExistException $e) {
// Allow as this just means there is no pending transfer
}

try {
$transfer = $this->transferMapper->getByTarget($targetUserId);
} catch (DoesNotExistException $e) {
// Allow as this just means there is no pending transfer
}

if (!empty($transfer)) {
return new DataResponse([
'status' => $transfer->getStatus(),
'source' => $transfer->getSource(),
'target' => $transfer->getTarget(),
], Http::STATUS_ACCEPTED);
}

$this->transferService->addTransferJob($author, $sourceUser, $targetUserId);
return new DataResponse([], Http::STATUS_CREATED);
}
}
46 changes: 46 additions & 0 deletions lib/Db/Transfer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Guests\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method void setAuthor(string $userId)
* @method string getAuthor()
*
* @method void setSource(string $userId)
* @method string getSource()
*
* @method void setTarget(string $userId)
* @method string getTarget()
*
* @method void setStatus(string $status)
* @method string getStatus()
*/
class Transfer extends Entity {
public const STATUS_WAITING = 'waiting';
public const STATUS_STARTED = 'started';

/** @var string */
protected $author;
/** @var string */
protected $source;
/** @var string */
protected $target;
/** @var string */
protected $status;

public function __construct() {
$this->addType('author', 'string');
$this->addType('source', 'string');
$this->addType('target', 'string');
$this->addType('status', 'string');
}
}
Loading

0 comments on commit 51e2a57

Please sign in to comment.