diff --git a/appinfo/routes.php b/appinfo/routes.php index 9752c36db..675f5257c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -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'], diff --git a/lib/Controller/BoardOcsController.php b/lib/Controller/BoardOcsController.php index 578693903..84f28e100 100644 --- a/lib/Controller/BoardOcsController.php +++ b/lib/Controller/BoardOcsController.php @@ -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); } diff --git a/lib/Controller/CardOcsController.php b/lib/Controller/CardOcsController.php index d0a4fe70e..9e1869734 100644 --- a/lib/Controller/CardOcsController.php +++ b/lib/Controller/CardOcsController.php @@ -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; @@ -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, @@ -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] diff --git a/lib/Db/Assignment.php b/lib/Db/Assignment.php index 4160aed12..ee4062bd1 100644 --- a/lib/Db/Assignment.php +++ b/lib/Db/Assignment.php @@ -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'); diff --git a/lib/Db/AssignmentMapper.php b/lib/Db/AssignmentMapper.php index da1af6405..8cbb43c61 100644 --- a/lib/Db/AssignmentMapper.php +++ b/lib/Db/AssignmentMapper.php @@ -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; @@ -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 { @@ -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; } diff --git a/lib/Db/FederatedUser.php b/lib/Db/FederatedUser.php index c48c9eb81..31f2bfff0 100644 --- a/lib/Db/FederatedUser.php +++ b/lib/Db/FederatedUser.php @@ -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(), diff --git a/lib/Service/AssignmentService.php b/lib/Service/AssignmentService.php index 4d9a97486..89849ee47 100644 --- a/lib/Service/AssignmentService.php +++ b/lib/Service/AssignmentService.php @@ -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'); } diff --git a/lib/Service/ExternalBoardService.php b/lib/Service/ExternalBoardService.php index 6125c63b3..437b03caf 100644 --- a/lib/Service/ExternalBoardService.php +++ b/lib/Service/ExternalBoardService.php @@ -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 { @@ -25,6 +29,7 @@ public function __construct( private BoardService $boardService, private PermissionService $permissionService, private BoardMapper $boardMapper, + private IURLGenerator $urlGenerator, private ?string $userId, ) { } @@ -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; @@ -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, @@ -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, diff --git a/lib/Service/PermissionService.php b/lib/Service/PermissionService.php index ba99ec44a..4de532736 100644 --- a/lib/Service/PermissionService.php +++ b/lib/Service/PermissionService.php @@ -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; @@ -27,7 +29,7 @@ class PermissionService { - /** @var array> */ + /** @var array> */ private $users = []; // accessToken to check permission for federated shares @@ -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(); @@ -263,7 +266,7 @@ public function getUserId(): ?string { * * @param $boardId * @param $refresh - * @return array + * @return array * */ public function findUsers($boardId, $refresh = false) { // cache users of a board so we don't query them for every cards @@ -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()); diff --git a/src/components/board/SharingTabSidebar.vue b/src/components/board/SharingTabSidebar.vue index 2effaba2c..3d0a3de03 100644 --- a/src/components/board/SharingTabSidebar.vue +++ b/src/components/board/SharingTabSidebar.vue @@ -90,7 +90,7 @@ const SOURCE_TO_SHARE_TYPE = { groups: 1, emails: 4, remotes: 6, - teams: 7, + circles: 7, } export default { @@ -139,6 +139,7 @@ export default { 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] } return res }).slice(0, 10) diff --git a/src/components/cards/AvatarList.vue b/src/components/cards/AvatarList.vue index e2d4a8d47..34f29739b 100644 --- a/src/components/cards/AvatarList.vue +++ b/src/components/cards/AvatarList.vue @@ -19,6 +19,12 @@ :disable-menu="true" :hide-status="true" :size="32" /> + 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) diff --git a/src/services/CardApi.js b/src/services/CardApi.js index d9491f1f5..99b141a3f 100644 --- a/src/services/CardApi.js +++ b/src/services/CardApi.js @@ -110,11 +110,11 @@ export class CardApi { }) } - assignUser(cardId, id, type) { - return axios.post(this.url(`/cards/${cardId}/assign`), { userId: id, type }) + assignUser(cardId, id, type, boardId) { + return axios.post(this.ocsUrl(`/cards/${cardId}/assign`), { userId: id, type, boardId }) .then( (response) => { - return Promise.resolve(response.data) + return Promise.resolve(response.data.ocs.data) }, (err) => { return Promise.reject(err) @@ -125,11 +125,11 @@ export class CardApi { }) } - removeUser(cardId, id, type) { - return axios.put(this.url(`/cards/${cardId}/unassign`), { userId: id, type }) + removeUser(cardId, id, type, boardId) { + return axios.put(this.ocsUrl(`/cards/${cardId}/unassign`), { userId: id, type, boardId }) .then( (response) => { - return Promise.resolve(response.data) + return Promise.resolve(response.data.ocs.data) }, (err) => { return Promise.reject(err) diff --git a/src/store/card.js b/src/store/card.js index e9d0098a9..2a2cacd48 100644 --- a/src/store/card.js +++ b/src/store/card.js @@ -342,11 +342,13 @@ export default function cardModuleFactory() { commit('updateCardProperty', { property: 'done', card: updatedCard }) }, async assignCardToUser({ commit }, { card, assignee }) { - const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type) + const boardId = this.state.currentBoard.id + const user = await apiClient.assignUser(card.id, assignee.userId, assignee.type, boardId) commit('assignCardToUser', user) }, async removeUserFromCard({ commit }, { card, assignee }) { - const user = await apiClient.removeUser(card.id, assignee.userId, assignee.type) + const boardId = this.state.currentBoard.id + const user = await apiClient.removeUser(card.id, assignee.userId, assignee.type, boardId) commit('removeUserFromCard', user) }, async addLabel({ commit }, data) { diff --git a/src/store/main.js b/src/store/main.js index 857d31073..9fddd8e4b 100644 --- a/src/store/main.js +++ b/src/store/main.js @@ -87,7 +87,7 @@ export default function storeFactory() { }, assignables: state => { return [ - ...state.assignableUsers.map((user) => ({ ...user, type: 0 })), + ...state.assignableUsers.map((user) => ({ ...user, type: user.type })), ...state.currentBoard.acl.filter((acl) => acl.type === 1 && typeof acl.participant === 'object').map((group) => ({ ...group.participant, type: 1 })), ...state.currentBoard.acl.filter((acl) => acl.type === 7 && typeof acl.participant === 'object').map((circle) => ({ ...circle.participant, type: 7 })), ] diff --git a/tests/unit/Service/PermissionServiceTest.php b/tests/unit/Service/PermissionServiceTest.php index 05139f5fa..3099b9edd 100644 --- a/tests/unit/Service/PermissionServiceTest.php +++ b/tests/unit/Service/PermissionServiceTest.php @@ -32,6 +32,7 @@ use OCA\Deck\Db\User; use OCA\Deck\NoPermissionException; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Federation\ICloudIdManager; use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; @@ -53,6 +54,7 @@ class PermissionServiceTest extends \Test\TestCase { private IGroupManager|MockObject $groupManager; private MockObject|IManager $shareManager; private IConfig|MockObject $config; + private ICloudIdManager|MockObject $cloudIdManager; public function setUp(): void { parent::setUp(); @@ -65,6 +67,7 @@ public function setUp(): void { $this->groupManager = $this->createMock(IGroupManager::class); $this->shareManager = $this->createMock(IManager::class); $this->config = $this->createMock(IConfig::class); + $this->cloudIdManager = $this->createMock(ICloudIdManager::class); $this->service = new PermissionService( $this->logger, @@ -76,6 +79,7 @@ public function setUp(): void { $this->groupManager, $this->shareManager, $this->config, + $this->cloudIdManager, 'admin' ); }