Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@
['name' => 'card_ocs#create', 'url' => '/api/v{apiVersion}/cards', 'verb' => 'POST'],
['name' => 'card_ocs#update', 'url' => '/api/v{apiVersion}/cards/{cardId}', 'verb' => 'PUT'],
['name' => 'card_ocs#assignLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'POST'],
['name' => 'card_ocs#assignUser', 'url' => '/api/v{apiVersion}/cards/{cardId}/assign', 'verb' => 'POST'],
['name' => 'card_ocs#unAssignUser', 'url' => '/api/v{apiVersion}/cards/{cardId}/unassign', 'verb' => 'PUT'],
['name' => 'card_ocs#removeLabel', 'url' => '/api/v{apiVersion}/cards/{cardId}/label/{labelId}', 'verb' => 'DELETE'],

['name' => 'stack_ocs#create', 'url' => '/api/v{apiVersion}/stacks', 'verb' => 'POST'],
Expand Down
2 changes: 0 additions & 2 deletions lib/Controller/BoardOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,10 @@ public function index(): DataResponse {
#[NoCSRFRequired]
#[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)]
public function read(int $boardId): DataResponse {
// Board on this instance -> get it from database
$localBoard = $this->boardService->find($boardId, true, true);
if ($localBoard->getExternalId() !== null) {
return $this->externalBoardService->getExternalBoardFromRemote($localBoard);
}
// Board on other instance -> get it from other instance
return new DataResponse($localBoard);
}

Expand Down
28 changes: 28 additions & 0 deletions lib/Controller/CardOcsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace OCA\Deck\Controller;

use OCA\Deck\Model\OptionalNullableValue;
use OCA\Deck\Service\AssignmentService;
use OCA\Deck\Service\BoardService;
use OCA\Deck\Service\CardService;
use OCA\Deck\Service\ExternalBoardService;
Expand All @@ -25,6 +26,7 @@ public function __construct(
string $appName,
IRequest $request,
private CardService $cardService,
private AssignmentService $assignmentService,
private StackService $stackService,
private BoardService $boardService,
private ExternalBoardService $externalBoardService,
Expand Down Expand Up @@ -77,6 +79,32 @@ public function assignLabel(?int $boardId, int $cardId, int $labelId): DataRespo
return new DataResponse($this->cardService->assignLabel($cardId, $labelId));
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function assignUser(?int $boardId, int $cardId, string $userId, int $type = 0): DataResponse {
if ($boardId) {
$localBoard = $this->boardService->find($boardId, false);
if ($localBoard->getExternalId()) {
return new DataResponse($this->externalBoardService->assignUserOnRemote($localBoard, $cardId, $userId, $type));
}
}
return new DataResponse($this->assignmentService->assignUser($cardId, $userId, $type));
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
public function unAssignUser(?int $boardId, int $cardId, string $userId, int $type = 0): DataResponse {
if ($boardId) {
$localBoard = $this->boardService->find($boardId, false);
if ($localBoard->getExternalId()) {
return new DataResponse($this->externalBoardService->unAssignUserOnRemote($localBoard, $cardId, $userId, $type));
}
}
return new DataResponse($this->assignmentService->unAssignUser($cardId, $userId, $type));
}

#[NoAdminRequired]
#[PublicPage]
#[NoCSRFRequired]
Expand Down
1 change: 1 addition & 0 deletions lib/Db/Assignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Assignment extends RelationalEntity implements JsonSerializable {
public const TYPE_USER = Acl::PERMISSION_TYPE_USER;
public const TYPE_GROUP = Acl::PERMISSION_TYPE_GROUP;
public const TYPE_CIRCLE = Acl::PERMISSION_TYPE_CIRCLE;
public const TYPE_REMOTE = Acl::PERMISSION_TYPE_REMOTE;

public function __construct() {
$this->addType('id', 'integer');
Expand Down
10 changes: 9 additions & 1 deletion lib/Db/AssignmentMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\Deck\Service\CirclesService;
use OCP\AppFramework\Db\Entity;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Federation\ICloudIdManager;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
Expand All @@ -29,13 +30,17 @@ class AssignmentMapper extends DeckMapper implements IPermissionMapper {
/** @var CirclesService */
private $circleService;

public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager, IGroupManager $groupManager, CirclesService $circleService) {
/** @var ICloudIdManager */
private $cloudIdManager;

public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager, IGroupManager $groupManager, CirclesService $circleService, ICloudIdManager $cloudIdManager) {
parent::__construct($db, 'deck_assigned_users', Assignment::class);

$this->cardMapper = $cardMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->circleService = $circleService;
$this->cloudIdManager = $cloudIdManager;
}

public function findAll(int $cardId): array {
Expand Down Expand Up @@ -175,6 +180,9 @@ private function getOrigin(Assignment $assignment) {
$origin = $this->circleService->getCircle($assignment->getParticipant());
return $origin ? new Circle($origin) : null;
}
if ($assignment->getType() === Assignment::TYPE_REMOTE) {
return new FederatedUser($this->cloudIdManager->resolveCloudId($assignment->getParticipant()));
}
return null;
}

Expand Down
8 changes: 8 additions & 0 deletions lib/Db/FederatedUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public function __construct(ICloudId $cloudId) {
parent::__construct($cloudId->getId(), $cloudId);
}

public function getCloudId(): ICloudId {
return $this->cloudId;
}

public function getUID(): string {
return $this->cloudId->getId();
}

public function getObjectSerialization(): array {
return [
'uid' => $this->cloudId->getId(),
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/AssignmentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public function __construct(
public function assignUser(int $cardId, string $userId, int $type = Assignment::TYPE_USER): Assignment {
$this->assignmentServiceValidator->check(compact('cardId', 'userId'));

if ($type !== Assignment::TYPE_USER && $type !== Assignment::TYPE_GROUP) {
if ($type !== Assignment::TYPE_USER && $type !== Assignment::TYPE_GROUP && $type !== Assignment::TYPE_REMOTE) {
throw new BadRequestException('Invalid type provided for assignemnt');
}

Expand Down
101 changes: 101 additions & 0 deletions lib/Service/ExternalBoardService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
namespace OCA\Deck\Service;

use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Assignment;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\FederatedUser;
use OCA\Deck\Db\User;
use OCA\Deck\Federation\DeckFederationProxy;
use OCA\Deck\Model\OptionalNullableValue;
use OCP\AppFramework\Http\DataResponse;
use OCP\Federation\ICloudIdManager;
use OCP\IURLGenerator;
use OCP\IUserManager;

class ExternalBoardService {
Expand All @@ -25,6 +29,7 @@ public function __construct(
private BoardService $boardService,
private PermissionService $permissionService,
private BoardMapper $boardMapper,
private IURLGenerator $urlGenerator,
private ?string $userId,
) {
}
Expand All @@ -50,9 +55,37 @@ public function getExternalStacksFromRemote(Board $localBoard):DataResponse {
return new DataResponse($this->LocalizeRemoteStacks($ocs, $localBoard));
}

public function localizeRemoteUser(Board $localBoard, array $user): array|User|FederatedUser|null {
// skip invalid users
if (!$user['uid']) {
return null;
}
// if it's already a valid cloud id the user originates from a third instance and we pass it as is
if ($this->cloudIdManager->isValidCloudId($user['uid'])) {
if ($user['remote'] == $this->urlGenerator->getBaseUrl()) {
// local user from remote: return as local user
$localuid = $this->cloudIdManager->resolveCloudId($user['uid'])->getUser();
return new User($localuid, $this->userManager);
}
return new FederatedUser($this->cloudIdManager->resolveCloudId($user['uid']));
}
// if it's not a valid cloud id: it originates from the remote instance and we send it out as a federated user
$owner = $localBoard->resolveOwner(); // retrieve owner to get the remote
if ($owner instanceof FederatedUser) {
return new FederatedUser($this->cloudIdManager->getCloudId($user['uid'], $owner->getCloudId()->getRemote()));
}
return null;
}

public function LocalizeRemoteStacks(array $stacks, Board $localBoard) {
foreach ($stacks as $i => $stack) {
$stack['boardId'] = $localBoard->getId();
foreach ($stack['cards'] as $j => $card) {
$stack['cards'][$j]['assignedUsers'] = array_map(function ($assignment) use ($localBoard) {
$assignment['participant'] = $this->localizeRemoteUser($localBoard, $assignment['participant']);
return $assignment;
}, $card['assignedUsers']);
}
$stacks[$i] = $stack;
}
return $stacks;
Expand All @@ -63,9 +96,19 @@ public function LocalizeRemoteBoard(array $remoteBoard, Board $localBoard) {
$remoteBoard['owner'] = $localBoard->resolveOwner();
$remoteBoard['acl'] = $localBoard->getAcl();
$remoteBoard['permissions'] = $localBoard->getPermissions();
$remoteBoard['users'] = $this->localizeRemoteUsers($remoteBoard['users'], $localBoard);
return $remoteBoard;
}

public function localizeRemoteUsers(array $users, Board $localBoard) {
$localizedUsers = [];
foreach ($users as $i => $user) {
$localizedUsers[] = $this->localizeRemoteUser($localBoard, $user);
}

return $localizedUsers;
}

public function createCardOnRemote(
Board $localBoard,
string $title,
Expand Down Expand Up @@ -159,6 +202,64 @@ public function removeLabelOnRemote(Board $localBoard, int $cardId, int $labelId
return $this->proxy->getOcsData($resp);
}

public function assignUserOnRemote(Board $localBoard, int $cardId, string $userId, int $type = 0): array {
$this->configService->ensureFederationEnabled();

$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());

if ($this->cloudIdManager->isValidCloudId($userId)) {
$cloudId = $this->cloudIdManager->resolveCloudId($userId);
// assignee's origin is the same as the board owner's origin: send as local user
if ($cloudId->getRemote() === $ownerCloudId->getRemote()) {
$userId = $cloudId->getUser();
$type = Assignment::TYPE_USER;
}
} else {
// local user for us = remote user for remote
$userId = $this->cloudIdManager->getCloudId($userId, null)->getId();
$type = Assignment::TYPE_REMOTE;
}
$shareToken = $localBoard->getShareToken();
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/assign';
$resp = $this->proxy->post($ownerCloudId->getId(), $shareToken, $url, [
'userId' => $userId,
'type' => $type,
'boardId' => $localBoard->getExternalId(),
]);
$result = $this->proxy->getOcsData($resp);
if (isset($result['participant'])) {
$result['participant'] = $this->localizeRemoteUser($localBoard, $result['participant']);
}
return $result;
}

public function unAssignUserOnRemote(Board $localBoard, int $cardId, string $userId, int $type = 0): array {
$this->configService->ensureFederationEnabled();

$ownerCloudId = $this->cloudIdManager->resolveCloudId($localBoard->getOwner());

if ($this->cloudIdManager->isValidCloudId($userId)) {
$cloudId = $this->cloudIdManager->resolveCloudId($userId);
// assignee's origin is the same as the board owner's origin: send as local user
if ($cloudId->getRemote() === $ownerCloudId->getRemote()) {
$userId = $cloudId->getUser();
$type = Assignment::TYPE_USER;
}
} else {
// local user for us = remote user for remote
$userId = $this->cloudIdManager->getCloudId($userId, null)->getId();
$type = Assignment::TYPE_REMOTE;
}
$shareToken = $localBoard->getShareToken();
$url = $ownerCloudId->getRemote() . '/ocs/v2.php/apps/deck/api/v1.0/cards/' . $cardId . '/unassign';
$resp = $this->proxy->put($ownerCloudId->getId(), $shareToken, $url, [
'userId' => $userId,
'type' => $type,
'boardId' => $localBoard->getExternalId(),
]);
return $this->proxy->getOcsData($resp);
}

public function createStackOnRemote(
Board $localBoard,
string $title,
Expand Down
11 changes: 9 additions & 2 deletions lib/Service/PermissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Db\FederatedUser;
use OCA\Deck\Db\IPermissionMapper;
use OCA\Deck\Db\User;
use OCA\Deck\NoPermissionException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\Cache\CappedMemoryCache;
use OCP\Federation\ICloudIdManager;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUserManager;
Expand All @@ -27,7 +29,7 @@

class PermissionService {

/** @var array<string, array<string, User>> */
/** @var array<string, array<string, User|FederatedUser>> */
private $users = [];

// accessToken to check permission for federated shares
Expand All @@ -47,6 +49,7 @@ public function __construct(
private IGroupManager $groupManager,
private IManager $shareManager,
private IConfig $config,
private ICloudIdManager $cloudIdManager,
private ?string $userId,
) {
$this->boardCache = new CappedMemoryCache();
Expand Down Expand Up @@ -263,7 +266,7 @@ public function getUserId(): ?string {
*
* @param $boardId
* @param $refresh
* @return array<string, User>
* @return array<string, User | FederatedUser>
* */
public function findUsers($boardId, $refresh = false) {
// cache users of a board so we don't query them for every cards
Expand Down Expand Up @@ -306,6 +309,10 @@ public function findUsers($boardId, $refresh = false) {
}
}

if ($acl->getType() === Acl::PERMISSION_TYPE_REMOTE) {
$users[(string)$acl->getParticipant()] = new FederatedUser($this->cloudIdManager->resolveCloudId($acl->getParticipant()));
}

if ($this->circlesService->isCirclesEnabled() && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) {
try {
$circle = $this->circlesService->getCircle($acl->getParticipant());
Expand Down
3 changes: 2 additions & 1 deletion src/components/board/SharingTabSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
groups: 1,
emails: 4,
remotes: 6,
teams: 7,
circles: 7,
}

export default {
Expand Down Expand Up @@ -139,6 +139,7 @@
displayName: item.displayname || item.name || item.label || item.id,
user: item.id,
subname: item.shareWithDisplayNameUnique || item.subline || item.id, // NcSelectUser does its own pattern matching to filter things out
type: SOURCE_TO_SHARE_TYPE[item.source]

Check warning on line 142 in src/components/board/SharingTabSidebar.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Missing trailing comma
}
return res
}).slice(0, 10)
Expand Down
6 changes: 6 additions & 0 deletions src/components/cards/AvatarList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
:disable-menu="true"
:hide-status="true"
:size="32" />
<NcAvatar v-if="user.type === 6"
:user="user.participant.uid"
:display-name="user.participant.displayname"
:disable-menu="true"
:hide-status="true"
:size="32" />
<NcAvatar v-if="user.type === 1"
:user="user.participant.uid"
:display-name="user.participant.displayname"
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/CardMenuEntries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default {
return !!board?.permissions?.PERMISSION_EDIT
},
isCurrentUserAssigned() {
return this.card.assignedUsers.find((item) => item.type === 0 && item.participant.uid === getCurrentUser()?.uid)
return this.card.assignedUsers.find((item) => (item.type === 0 || item.type === 6) && item.participant.uid === getCurrentUser()?.uid)
},
boardId() {
return this.card?.boardId ? this.card.boardId : Number(this.$route.params.id)
Expand Down
Loading
Loading