From fcf2c549378a97dfb5e90bb5e5367717305ab941 Mon Sep 17 00:00:00 2001 From: Hoang Pham Date: Mon, 18 Nov 2024 16:40:42 +0700 Subject: [PATCH] share with teams Signed-off-by: Hoang Pham --- lib/Capabilities.php | 11 ++- lib/Helper/CircleHelper.php | 89 ++++++++++++++++++ lib/Helper/UserHelper.php | 9 ++ lib/Service/PermissionsService.php | 43 ++++++++- lib/Service/ShareService.php | 32 ++++++- openapi.json | 6 +- package-lock.json | 2 + package.json | 1 + src/modules/sidebar/mixins/shareAPI.js | 43 ++++++++- src/modules/sidebar/partials/ShareForm.vue | 91 +++++++++++++++---- src/modules/sidebar/partials/ShareList.vue | 28 +++--- src/shared/mixins/searchUserGroup.js | 29 +++--- src/shared/mixins/shareTypesMixin.js | 9 ++ src/types/openapi/openapi.ts | 1 + tests/unit/Service/PermissionsServiceTest.php | 4 +- 15 files changed, 342 insertions(+), 56 deletions(-) create mode 100644 lib/Helper/CircleHelper.php diff --git a/lib/Capabilities.php b/lib/Capabilities.php index dea9ddd49..e4971a2fb 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -6,6 +6,7 @@ namespace OCA\Tables; +use OCA\Tables\Helper\CircleHelper; use OCP\App\IAppManager; use OCP\Capabilities\ICapability; use OCP\IConfig; @@ -18,18 +19,23 @@ */ class Capabilities implements ICapability { private IAppManager $appManager; + private LoggerInterface $logger; + private IConfig $config; - public function __construct(IAppManager $appManager, LoggerInterface $logger, IConfig $config) { + private CircleHelper $circleHelper; + + public function __construct(IAppManager $appManager, LoggerInterface $logger, IConfig $config, CircleHelper $circleHelper) { $this->appManager = $appManager; $this->logger = $logger; $this->config = $config; + $this->circleHelper = $circleHelper; } /** * - * @return array{tables: array{enabled: bool, version: string, apiVersions: string[], features: string[], column_types: string[]}} + * @return array{tables: array{enabled: bool, version: string, apiVersions: string[], features: string[], isCirclesEnabled: bool, column_types: string[]}} * * @inheritDoc */ @@ -52,6 +58,7 @@ public function getCapabilities(): array { 'favorite', 'archive', ], + 'isCirclesEnabled' => $this->circleHelper->isCirclesEnabled(), 'column_types' => [ 'text-line', $textColumnVariant, diff --git a/lib/Helper/CircleHelper.php b/lib/Helper/CircleHelper.php new file mode 100644 index 000000000..6babfde55 --- /dev/null +++ b/lib/Helper/CircleHelper.php @@ -0,0 +1,89 @@ +logger = $logger; + $this->circlesEnabled = $appManager->isEnabledForUser('circles'); + if ($this->circlesEnabled) { + try { + $this->circlesManager = $circlesManager ?? Server::get(CirclesManager::class); + } catch (Throwable $e) { + $this->logger->warning('Failed to get CirclesManager: ' . $e->getMessage()); + $this->circlesManager = null; + $this->circlesEnabled = false; + } + } else { + $this->circlesManager = null; + } + } + + public function isCirclesEnabled(): bool { + return $this->circlesEnabled; + } + + public function getCircleDisplayName(string $circleId, string $userId): string { + if (!$this->circlesEnabled) { + return $circleId; + } + + try { + $federatedUser = $this->circlesManager->getFederatedUser($userId, Member::TYPE_USER); + $this->circlesManager->startSession($federatedUser); + + $circle = $this->circlesManager->getCircle($circleId); + return $circle ? ($circle->getDisplayName() ?: $circleId) : $circleId; + } catch (Throwable $e) { + $this->logger->warning('Failed to get circle display name: ' . $e->getMessage(), [ + 'circleId' => $circleId, + 'userId' => $userId + ]); + return $circleId; + } + } + + public function getUserCircles(string $userId): array { + if (!$this->circlesEnabled) { + return []; + } + + try { + $federatedUser = $this->circlesManager->getFederatedUser($userId, Member::TYPE_USER); + $this->circlesManager->startSession($federatedUser); + $probe = new CircleProbe(); + $probe->mustBeMember(); + return $this->circlesManager->getCircles($probe); + } catch (Throwable $e) { + $this->logger->warning('Failed to get user circles: ' . $e->getMessage()); + return []; + } + } +} diff --git a/lib/Helper/UserHelper.php b/lib/Helper/UserHelper.php index 3596af057..9b98197a2 100644 --- a/lib/Helper/UserHelper.php +++ b/lib/Helper/UserHelper.php @@ -14,17 +14,26 @@ use OCP\IUserManager; use Psr\Log\LoggerInterface; +/** + * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedDocblockClass + */ class UserHelper { private IUserManager $userManager; private LoggerInterface $logger; + private IGroupManager $groupManager; + /** + * @psalm-suppress UndefinedClass + */ public function __construct(IUserManager $userManager, LoggerInterface $logger, IGroupManager $groupManager) { $this->userManager = $userManager; $this->logger = $logger; $this->groupManager = $groupManager; } + public function getUserDisplayName(string $userId): string { try { $user = $this->getUser($userId); diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index cacf99abe..7372302bb 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -18,6 +18,7 @@ use OCA\Tables\Db\ViewMapper; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Helper\CircleHelper; use OCA\Tables\Helper\ConversionHelper; use OCA\Tables\Helper\UserHelper; use OCA\Tables\Model\Permissions; @@ -25,7 +26,11 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception; use Psr\Log\LoggerInterface; +use Throwable; +/** + * @psalm-suppress UndefinedDocblockClass + */ class PermissionsService { private TableMapper $tableMapper; @@ -35,11 +40,14 @@ class PermissionsService { private UserHelper $userHelper; + private CircleHelper $circleHelper; + protected LoggerInterface $logger; protected ?string $userId = null; protected bool $isCli = false; + private ContextMapper $contextMapper; public function __construct( @@ -50,6 +58,7 @@ public function __construct( ShareMapper $shareMapper, ContextMapper $contextMapper, UserHelper $userHelper, + CircleHelper $circleHelper, bool $isCLI ) { $this->tableMapper = $tableMapper; @@ -60,6 +69,7 @@ public function __construct( $this->userId = $userId; $this->isCli = $isCLI; $this->contextMapper = $contextMapper; + $this->circleHelper = $circleHelper; } @@ -420,6 +430,7 @@ public function canReadShare(Share $share, ?string $userId = null): bool { * @param int $elementId * @param 'table'|'view' $elementType * @param string $userId + * @return Permissions * @throws NotFoundError */ public function getSharedPermissionsIfSharedWithMe(int $elementId, string $elementType, string $userId): Permissions { @@ -436,16 +447,40 @@ public function getSharedPermissionsIfSharedWithMe(int $elementId, string $eleme $this->logger->warning('Exception occurred: '.$e->getMessage().' Permission denied.'); return new Permissions(); } - $additionalShares = []; + $groupShares = []; foreach ($userGroups as $userGroup) { try { - $additionalShares[] = $this->shareMapper->findAllSharesForNodeFor($elementType, $elementId, $userGroup->getGid(), 'group'); + $groupShares[] = $this->shareMapper->findAllSharesForNodeFor($elementType, $elementId, $userGroup->getGid(), 'group'); } catch (Exception $e) { $this->logger->warning('Exception occurred: '.$e->getMessage().' Permission denied.'); return new Permissions(); } } - $shares = array_merge($shares, ...$additionalShares); + + $shares = array_merge($shares, ...$groupShares); + + if ($this->circleHelper->isCirclesEnabled()) { + $circleShares = []; + + try { + $userCircles = $this->circleHelper->getUserCircles($userId); + } catch (Throwable $e) { + $this->logger->warning('Exception occurred: ' . $e->getMessage() . ' Permission denied.'); + return new Permissions(); + } + + foreach ($userCircles as $userCircle) { + try { + $circleShares[] = $this->shareMapper->findAllSharesForNodeFor($elementType, $elementId, $userCircle->getSingleId(), 'circle'); + } catch (Exception $e) { + $this->logger->warning('Exception occurred: ' . $e->getMessage() . ' Permission denied.'); + return new Permissions(); + } + } + + $shares = array_merge($shares, ...$circleShares); + } + if (count($shares) > 0) { $read = array_reduce($shares, function ($carry, $share) { return $carry || ($share->getPermissionRead()); @@ -520,7 +555,7 @@ private function hasPermission(int $existingPermissions, string $permissionName) $constantName = 'PERMISSION_' . strtoupper($permissionName); try { $permissionBit = constant(Application::class . "::$constantName"); - } catch (\Throwable $t) { + } catch (Throwable $t) { $this->logger->error('Unexpected permission string {permission}', [ 'app' => Application::APP_ID, 'permission' => $permissionName, diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index 53f39ae83..033f2a1fc 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -24,9 +24,9 @@ use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; +use OCA\Tables\Helper\CircleHelper; use OCA\Tables\Helper\GroupHelper; use OCA\Tables\Helper\UserHelper; - use OCA\Tables\Model\Permissions; use OCA\Tables\ResponseDefinitions; use OCP\AppFramework\Db\DoesNotExistException; @@ -35,9 +35,11 @@ use OCP\DB\Exception; use OCP\IDBConnection; use Psr\Log\LoggerInterface; +use Throwable; /** * @psalm-import-type TablesShare from ResponseDefinitions + * @psalm-suppress UndefinedDocblockClass */ class ShareService extends SuperService { use TTransactional; @@ -51,7 +53,11 @@ class ShareService extends SuperService { protected UserHelper $userHelper; protected GroupHelper $groupHelper; + + protected CircleHelper $circleHelper; + private ContextNavigationMapper $contextNavigationMapper; + private IDBConnection $dbc; public function __construct( @@ -63,6 +69,7 @@ public function __construct( ViewMapper $viewMapper, UserHelper $userHelper, GroupHelper $groupHelper, + CircleHelper $circleHelper, ContextNavigationMapper $contextNavigationMapper, IDBConnection $dbc, ) { @@ -72,6 +79,7 @@ public function __construct( $this->viewMapper = $viewMapper; $this->userHelper = $userHelper; $this->groupHelper = $groupHelper; + $this->circleHelper = $circleHelper; $this->contextNavigationMapper = $contextNavigationMapper; $this->dbc = $dbc; } @@ -163,7 +171,17 @@ private function findElementsSharedWithMe(string $elementType = 'table', ?string $shares = $this->mapper->findAllSharesFor($elementType, $userGroup->getGid(), $userId, 'group'); $elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares); } - } catch (Exception $e) { + + // get all views or tables that are shared with me by circle + if ($this->circleHelper->isCirclesEnabled()) { + $userCircles = $this->circleHelper->getUserCircles($userId); + + foreach ($userCircles as $userCircle) { + $shares = $this->mapper->findAllSharesFor($elementType, $userCircle->getSingleId(), $userId, 'circle'); + $elementsSharedWithMe = array_merge($elementsSharedWithMe, $shares); + } + } + } catch (Throwable $e) { throw new InternalError($e->getMessage()); } foreach ($elementsSharedWithMe as $share) { @@ -398,6 +416,16 @@ private function addReceiverDisplayName(Share $share):Share { $share->setReceiverDisplayName($this->userHelper->getUserDisplayName($share->getReceiver())); } elseif ($share->getReceiverType() === 'group') { $share->setReceiverDisplayName($this->groupHelper->getGroupDisplayName($share->getReceiver())); + } elseif ($share->getReceiverType() === 'circle') { + if ($this->circleHelper->isCirclesEnabled()) { + $share->setReceiverDisplayName($this->circleHelper->getCircleDisplayName($share->getReceiver(), $this->userId)); + } else { + $this->logger->info( + 'Could not get display name for receiver type {type}', + ['type' => $share->getReceiverType()] + ); + $share->setReceiverDisplayName($share->getReceiver()); + } } else { $this->logger->info('can not use receiver type to get display name'); $share->setReceiverDisplayName($share->getReceiver()); diff --git a/openapi.json b/openapi.json index fddafc065..dfe6a88ca 100644 --- a/openapi.json +++ b/openapi.json @@ -33,7 +33,8 @@ "version", "apiVersions", "features", - "column_types" + "column_types", + "isCirclesEnabled" ], "properties": { "enabled": { @@ -59,6 +60,9 @@ "items": { "type": "string" } + }, + "isCirclesEnabled": { + "type": "boolean" } } } diff --git a/package-lock.json b/package-lock.json index 2e4f0af26..9da623a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.4.0", "@nextcloud/axios": "^2.5.1", + "@nextcloud/capabilities": "^1.2.0", "@nextcloud/dialogs": "^6.0.1", "@nextcloud/event-bus": "^3.3.1", "@nextcloud/files": "^3.10.0", @@ -2631,6 +2632,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@nextcloud/capabilities/-/capabilities-1.2.0.tgz", "integrity": "sha512-L1NQtOfHWzkfj0Ple1MEJt6HmOHWAi3y4qs+OnwSWexqJT0DtXTVPyRxi7ADyITwRxS5H9R/HMl6USAj4Nr1nQ==", + "license": "GPL-3.0-or-later", "dependencies": { "@nextcloud/initial-state": "^2.1.0" }, diff --git a/package.json b/package.json index 6871719d6..4623d4f15 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@mdi/svg": "^7.4.47", "@nextcloud/auth": "^2.4.0", "@nextcloud/axios": "^2.5.1", + "@nextcloud/capabilities": "^1.2.0", "@nextcloud/dialogs": "^6.0.1", "@nextcloud/event-bus": "^3.3.1", "@nextcloud/files": "^3.10.0", diff --git a/src/modules/sidebar/mixins/shareAPI.js b/src/modules/sidebar/mixins/shareAPI.js index a60e32665..27b5a2de6 100644 --- a/src/modules/sidebar/mixins/shareAPI.js +++ b/src/modules/sidebar/mixins/shareAPI.js @@ -6,8 +6,11 @@ import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import '@nextcloud/dialogs/style.css' import displayError from '../../../shared/utils/displayError.js' +import ShareTypes from '../../../shared/mixins/shareTypesMixin.js' export default { + mixins: [ShareTypes], + methods: { async getSharedWithFromBE() { try { @@ -23,16 +26,23 @@ export default { return shares.concat(res.data) } } catch (e) { + console.error('Error fetching shares:', e) displayError(e, t('tables', 'Could not fetch shares.')) + return [] } }, async sendNewShareToBE(share) { + if (!this.isValidShareType(share.shareType)) { + console.warn('Unsupported share type:', share.shareType) + return false + } + const data = { nodeType: this.isView ? 'view' : 'table', nodeId: this.activeElement.id, receiver: share.user, - receiverType: (share.isNoUser) ? 'group' : 'user', + receiverType: this.getReceiverType(share.shareType), permissionRead: true, permissionCreate: true, permissionUpdate: true, @@ -45,8 +55,11 @@ export default { displayError(e, t('tables', 'Could not create share.')) return false } - if (this.isView) await this.$store.dispatch('setViewHasShares', { viewId: this.activeElement.id, hasShares: true }) - else await this.$store.dispatch('setTableHasShares', { tableId: this.isView ? this.activeElement.tableId : this.activeElement.id, hasShares: true }) + if (this.isView) { + await this.$store.dispatch('setViewHasShares', { viewId: this.activeElement.id, hasShares: true }) + } else { + await this.$store.dispatch('setTableHasShares', { tableId: this.isView ? this.activeElement.tableId : this.activeElement.id, hasShares: true }) + } return true }, async removeShareFromBE(shareId) { @@ -64,5 +77,29 @@ export default { displayError(e, t('tables', 'Could not update share.')) } }, + + isValidShareType(shareType) { + if (shareType === this.SHARE_TYPES.SHARE_TYPE_CIRCLE && !this.isCirclesEnabled) { + return false + } + return [ + this.SHARE_TYPES.SHARE_TYPE_USER, + this.SHARE_TYPES.SHARE_TYPE_GROUP, + ...(this.isCirclesEnabled ? [this.SHARE_TYPES.SHARE_TYPE_CIRCLE] : []), + ].includes(shareType) + }, + + getReceiverType(shareType) { + switch (shareType) { + case this.SHARE_TYPES.SHARE_TYPE_USER: + return 'user' + case this.SHARE_TYPES.SHARE_TYPE_GROUP: + return 'group' + case this.SHARE_TYPES.SHARE_TYPE_CIRCLE: + return 'circle' + default: + throw new Error('Invalid share type') + } + }, }, } diff --git a/src/modules/sidebar/partials/ShareForm.vue b/src/modules/sidebar/partials/ShareForm.vue index 2d5151125..733b50dc5 100644 --- a/src/modules/sidebar/partials/ShareForm.vue +++ b/src/modules/sidebar/partials/ShareForm.vue @@ -4,12 +4,11 @@ -->