Files
nextcloud-tables/lib/Service/PermissionsService.php
Arthur Schiwon 1849033dbc style(PHP): adjust to codestyle updates
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
2025-02-10 12:14:18 +01:00

700 lines
20 KiB
PHP

<?php
/**
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Tables\Service;
use OCA\Tables\AppInfo\Application;
use OCA\Tables\Db\Context;
use OCA\Tables\Db\ContextMapper;
use OCA\Tables\Db\Share;
use OCA\Tables\Db\ShareMapper;
use OCA\Tables\Db\Table;
use OCA\Tables\Db\TableMapper;
use OCA\Tables\Db\View;
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;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception;
use Psr\Log\LoggerInterface;
use Throwable;
class PermissionsService {
private TableMapper $tableMapper;
private ViewMapper $viewMapper;
private ShareMapper $shareMapper;
private UserHelper $userHelper;
private CircleHelper $circleHelper;
protected LoggerInterface $logger;
protected ?string $userId = null;
protected bool $isCli = false;
private ContextMapper $contextMapper;
public function __construct(
LoggerInterface $logger,
?string $userId,
TableMapper $tableMapper,
ViewMapper $viewMapper,
ShareMapper $shareMapper,
ContextMapper $contextMapper,
UserHelper $userHelper,
CircleHelper $circleHelper,
bool $isCLI,
) {
$this->tableMapper = $tableMapper;
$this->viewMapper = $viewMapper;
$this->shareMapper = $shareMapper;
$this->userHelper = $userHelper;
$this->logger = $logger;
$this->userId = $userId;
$this->isCli = $isCLI;
$this->contextMapper = $contextMapper;
$this->circleHelper = $circleHelper;
}
/**
* @param string|null $userId
* @param bool $canBeEmpty
* @return string
*
* @throws InternalError
*/
public function preCheckUserId(?string $userId = null, bool $canBeEmpty = true): string {
if ($userId === null) {
$userId = $this->userId;
}
if ($userId === null) {
$e = new \Exception();
$error = 'PreCheck for userId failed, requested in ' . get_class($this) . '.';
$this->logger->debug($error, ['exception' => new \Exception()]);
throw new InternalError($error);
}
if ($userId === '' && !$this->isCli && !$canBeEmpty) {
$error = 'Try to set no user in context, but request is not allowed.';
$this->logger->warning($error);
throw new InternalError($error);
}
return $userId;
}
// ***** TABLES permissions *****
public function canReadTable(Table $table, ?string $userId = null): bool {
return $this->canReadColumnsByTableId($table->getId(), $userId);
}
/**
* @param Table $table
* @param string|null $userId
* @return bool
*/
public function canUpdateTable(Table $table, ?string $userId = null): bool {
try {
$userId = $this->preCheckUserId($userId);
} catch (InternalError $e) {
return false;
}
if ($userId === '') {
return true;
}
return $this->canManageTable($table, $userId);
}
public function canAccessNodeById(int $nodeType, int $nodeId, ?string $userId = null): bool {
if ($nodeType === Application::NODE_TYPE_TABLE) {
return $this->canReadColumnsByTableId($nodeId, $userId);
}
if ($nodeType === Application::NODE_TYPE_VIEW) {
return $this->canReadColumnsByViewId($nodeId, $userId);
}
return false;
}
public function canManageNodeById(int $nodeType, int $nodeId, ?string $userId = null): bool {
if ($nodeType === Application::NODE_TYPE_TABLE) {
return $this->canManageTableById($nodeId, $userId);
}
if ($nodeType === Application::NODE_TYPE_VIEW) {
return $this->canManageViewById($nodeId, $userId);
}
return false;
}
public function canManageContextById(int $contextId, ?string $userId = null): bool {
try {
$context = $this->contextMapper->findById($contextId, $userId);
} catch (DoesNotExistException $e) {
$this->logger->warning('Context does not exist');
return false;
} catch (MultipleObjectsReturnedException $e) {
$this->logger->warning('Multiple contexts found for this ID');
return false;
} catch (Exception $e) {
$this->logger->warning($e->getMessage());
return false;
}
if ($context->getOwnerType() !== Application::OWNER_TYPE_USER) {
$this->logger->warning('Unsupported owner type');
return false;
}
return $context->getOwnerId() === $userId || $this->canManageContext($context, $userId);
}
/**
* @throws NotFoundError
*/
public function canAccessContextById(int $contextId, ?string $userId = null): bool {
try {
$this->contextMapper->findById($contextId, $userId ?? $this->userId);
return true;
} catch (NotFoundError $e) {
return false;
}
}
public function canAccessView(View $view, ?string $userId = null): bool {
return $this->canAccessNodeById(Application::NODE_TYPE_VIEW, $view->getId(), $userId);
}
/**
* @param int $elementId
* @param string $nodeType
* @param string|null $userId
* @return bool
* @throws InternalError
* @note prefer canManageNodeById()
*/
public function canManageElementById(int $elementId, string $nodeType = 'table', ?string $userId = null): bool {
if ($nodeType === 'table') {
return $this->canManageTableById($elementId, $userId);
} elseif ($nodeType === 'view') {
return $this->canManageViewById($elementId, $userId);
} elseif ($nodeType === 'context') {
return $this->canManageContextById($elementId, $userId);
} else {
throw new InternalError('Cannot read permission for node type ' . $nodeType);
}
}
/**
* @param View $view
* @param string|null $userId
* @return bool
*/
public function canManageView(View $view, ?string $userId = null): bool {
return $this->checkPermission($view, 'view', 'manage', $userId);
}
public function canManageTable(Table $table, ?string $userId = null): bool {
return $this->checkPermission($table, 'table', 'manage', $userId);
}
public function canManageContext(Context $context, ?string $userId = null): bool {
return $this->checkPermission($context, 'context', 'manage', $userId);
}
public function canManageTableById(int $tableId, ?string $userId = null): bool {
try {
$table = $this->tableMapper->find($tableId);
} catch (MultipleObjectsReturnedException $e) {
$this->logger->warning('Multiple tables were found for this id');
return false;
} catch (DoesNotExistException $e) {
$this->logger->warning('No table was found for this id');
return false;
} catch (Exception $e) {
$this->logger->warning('Error occurred: ' . $e->getMessage());
return false;
}
return $this->canManageTable($table, $userId);
}
public function canManageViewById(int $viewId, ?string $userId = null): bool {
try {
$view = $this->viewMapper->find($viewId);
} catch (MultipleObjectsReturnedException $e) {
$this->logger->warning('Multiple tables were found for this id');
return false;
} catch (DoesNotExistException $e) {
$this->logger->warning('No table was found for this id');
return false;
} catch (InternalError|Exception $e) {
$this->logger->warning('Error occurred: ' . $e->getMessage());
return false;
}
return $this->canManageView($view, $userId);
}
// ***** COLUMNS permissions *****
public function canReadColumnsByViewId(int $viewId, ?string $userId = null): bool {
return $this->canReadRowsByElementId($viewId, 'view', $userId);
}
public function canReadColumnsByTableId(int $tableId, ?string $userId = null): bool {
$canReadRows = $this->checkPermissionById($tableId, 'table', 'read', $userId);
$canCreateRows = $this->checkPermissionById($tableId, 'table', 'create', $userId);
return $canCreateRows || $canReadRows;
}
/**
* @param Table $table
* @param string|null $userId
* @return bool
*/
public function canCreateColumns(Table $table, ?string $userId = null): bool {
return $this->canManageTable($table, $userId);
}
/**
* @param int $tableId
* @param string|null $userId
* @return bool
*/
public function canUpdateColumnsByTableId(int $tableId, ?string $userId = null): bool {
return $this->canManageTableById($tableId, $userId);
}
/**
* @param int $tableId
* @param string|null $userId
* @return bool
*/
public function canDeleteColumnsByTableId(int $tableId, ?string $userId = null): bool {
return $this->canManageTableById($tableId, $userId);
}
// ***** ROWS permissions *****
/**
* @param int $elementId
* @param 'table'|'view' $nodeType
* @param string|null $userId
* @return bool
*/
public function canReadRowsByElementId(int $elementId, string $nodeType, ?string $userId = null): bool {
return $this->checkPermissionById($elementId, $nodeType, 'read', $userId);
}
/**
* @param Table|View $element
* @param 'table'|'view' $nodeType
* @param string|null $userId
* @return bool
*/
public function canReadRowsByElement($element, string $nodeType, ?string $userId = null): bool {
return $this->checkPermission($element, $nodeType, 'read', $userId);
}
/**
* @param Table|View $element
* @param 'table'|'view' $nodeType
* @param string|null $userId
* @return bool
*/
public function canCreateRows($element, string $nodeType = 'view', ?string $userId = null): bool {
if ($nodeType === 'table') {
return $this->checkPermission($element, 'table', 'create', $userId);
}
return $this->checkPermission($element, 'view', 'create', $userId);
}
public function canCreateRowsById(int $nodeType, int $nodeId, ?string $userId = null): bool {
$sNodeType = ConversionHelper::constNodeType2String($nodeType);
return $this->checkPermissionById($nodeId, $sNodeType, 'create', $userId);
}
/**
* @param int $viewId
* @param string|null $userId
* @return bool
*/
public function canUpdateRowsByViewId(int $viewId, ?string $userId = null): bool {
return $this->checkPermissionById($viewId, 'view', 'update', $userId);
}
/**
* @param int $tableId
* @param string|null $userId
* @return bool
*/
public function canUpdateRowsByTableId(int $tableId, ?string $userId = null): bool {
return $this->checkPermissionById($tableId, 'table', 'update', $userId);
}
/**
* @param int $viewId
* @param string|null $userId
* @return bool
*/
public function canDeleteRowsByViewId(int $viewId, ?string $userId = null): bool {
return $this->checkPermissionById($viewId, 'view', 'delete', $userId);
}
/**
* @param int|null $tableId
* @param string|null $userId
* @return bool
*/
public function canDeleteRowsByTableId(?int $tableId = null, ?string $userId = null): bool {
if ($tableId === null) {
return false;
}
return $this->checkPermissionById($tableId, 'table', 'delete', $userId);
}
// ***** SHARE permissions *****
public function canReadShare(Share $share, ?string $userId = null): bool {
try {
$userId = $this->preCheckUserId($userId);
} catch (InternalError $e) {
$this->logger->warning('Cannot pre check the user id, permission denied');
return false;
}
if ($userId === '') {
return true;
}
try {
if ($this->canManageElementById($share->getNodeId(), $share->getNodeType())) {
return true;
}
} catch (InternalError $e) {
$this->logger->warning('Cannot check manage permissions, permission denied');
return false;
}
if ($share->getSender() === $userId) {
return true;
}
if ($share->getReceiverType() === 'user' && $share->getReceiver() === $userId) {
return true;
}
if ($share->getReceiverType() === 'group') {
try {
$userGroups = $this->userHelper->getGroupsForUser($userId);
foreach ($userGroups as $userGroup) {
if ($userGroup->getDisplayName() === $share->getReceiver()) {
return true;
}
}
} catch (InternalError $e) {
$this->logger->warning('Cannot get user groups, permission denied');
return false;
}
}
return false;
}
/**
* @param int $elementId
* @param 'table'|'view' $elementType
* @param string $userId
* @return Permissions
* @throws NotFoundError
*/
public function getSharedPermissionsIfSharedWithMe(int $elementId, string $elementType, string $userId): Permissions {
try {
$shares = $this->shareMapper->findAllSharesForNodeFor($elementType, $elementId, $userId);
} catch (Exception $e) {
$this->logger->warning('Exception occurred: ' . $e->getMessage() . ' Permission denied.');
return new Permissions();
}
try {
$userGroups = $this->userHelper->getGroupsForUser($userId);
} catch (InternalError $e) {
$this->logger->warning('Exception occurred: ' . $e->getMessage() . ' Permission denied.');
return new Permissions();
}
$groupShares = [];
foreach ($userGroups as $userGroup) {
try {
$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, ...$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());
}, false);
$create = array_reduce($shares, function ($carry, $share) {
return $carry || ($share->getPermissionCreate());
}, false);
$update = array_reduce($shares, function ($carry, $share) {
return $carry || ($share->getPermissionUpdate());
}, false);
$delete = array_reduce($shares, function ($carry, $share) {
return $carry || ($share->getPermissionDelete());
}, false);
$manage = array_reduce($shares, function ($carry, $share) {
return $carry || ($share->getPermissionManage());
}, false);
return new Permissions(
read: $read || $update || $delete || $manage,
create: $create || $manage,
update: $update || $manage,
delete: $delete || $manage,
manage: $manage,
);
}
throw new NotFoundError('No share for ' . $elementType . ' and given user ID found.');
}
// private methods ==========================================================================
/**
* @throws NotFoundError
*/
public function getPermissionIfAvailableThroughContext(int $nodeId, string $nodeType, string $userId): int {
$permissions = 0;
$found = false;
$iNodeType = ConversionHelper::stringNodeType2Const($nodeType);
$contexts = $this->contextMapper->findAllContainingNode($iNodeType, $nodeId, $userId);
foreach ($contexts as $context) {
$found = true;
if ($context->getOwnerType() === Application::OWNER_TYPE_USER
&& $context->getOwnerId() === $userId) {
// Making someone owner of a context, makes this person also having manage permissions on the node.
// This is sort of an intended "privilege escalation".
return Application::PERMISSION_ALL;
}
foreach ($context->getNodes() as $nodeRelation) {
$permissions |= $nodeRelation['permissions'];
}
}
if (!$found) {
throw new NotFoundError('Node not found in any context');
}
return $permissions;
}
/**
* @throws NotFoundError
*/
public function getPermissionArrayForNodeFromContexts(int $nodeId, string $nodeType, string $userId): Permissions {
$permissions = $this->getPermissionIfAvailableThroughContext($nodeId, $nodeType, $userId);
return new Permissions(
read: (bool)($permissions & Application::PERMISSION_READ),
create: (bool)($permissions & Application::PERMISSION_CREATE),
update: (bool)($permissions & Application::PERMISSION_UPDATE),
delete: (bool)($permissions & Application::PERMISSION_DELETE),
manage: (bool)($permissions & Application::PERMISSION_MANAGE),
);
}
private function hasPermission(int $existingPermissions, string $permissionName): bool {
$constantName = 'PERMISSION_' . strtoupper($permissionName);
try {
$permissionBit = constant(Application::class . "::$constantName");
} catch (Throwable $t) {
$this->logger->error('Unexpected permission string {permission}', [
'app' => Application::APP_ID,
'permission' => $permissionName,
'exception' => $t,
]);
return false;
}
return (bool)($existingPermissions & $permissionBit);
}
/**
* @param Table|View|Context $element
* @param 'table'|'view'|'context' $nodeType
* @param 'read'|'create'|'update'|'delete'|'manage'|'manageTable' $permission
* @param string|null $userId
* @return bool
*/
private function checkPermission(Table|View|Context $element, string $nodeType, string $permission, ?string $userId = null): bool {
if ($this->basisCheck($element, $nodeType, $userId)) {
return true;
}
if (!$userId) {
return false;
}
try {
return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)->$permission;
} catch (NotFoundError $e) {
try {
if ($nodeType !== 'context'
&& $this->hasPermission($this->getPermissionIfAvailableThroughContext($element->getId(), $nodeType, $userId), $permission)
) {
return true;
}
} catch (NotFoundError $e) {
}
$this->logger->info($e->getMessage(), ['exception' => $e]);
}
return false;
}
/**
* @param int $elementId
* @param 'table'|'view' $nodeType
* @param 'read'|'create'|'update'|'delete'|'manage'|'manageTable' $permission
* @param string|null $userId
* @return bool
*/
private function checkPermissionById(int $elementId, string $nodeType, string $permission, ?string $userId = null): bool {
if ($this->basisCheckById($elementId, $nodeType, $userId)) {
return true;
}
if ($userId) {
try {
return $this->getSharedPermissionsIfSharedWithMe($elementId, $nodeType, $userId)->$permission;
} catch (NotFoundError $e) {
try {
if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($elementId, $nodeType, $userId), $permission)) {
return true;
}
} catch (NotFoundError $e) {
}
$this->logger->info($e->getMessage(), ['exception' => $e]);
}
}
return false;
}
private function basisCheck(Table|View|Context $element, string $nodeType, ?string &$userId): bool {
try {
$userId = $this->preCheckUserId($userId);
} catch (InternalError $e) {
$e = new \Exception('Cannot pre check the user id');
$this->logger->error($e->getMessage(), ['exception' => $e]);
return false;
}
if ($userId === '') {
return true;
}
if ($this->userIsElementOwner($element, $userId, $nodeType)) {
return true;
}
try {
$permissions = $this->getSharedPermissionsIfSharedWithMe($nodeType === 'view' ? $element->getTableId() : $element->getId(), 'table', $userId);
if ($permissions->manage) {
return true;
}
} catch (NotFoundError $e) {
return false;
}
return false;
}
/**
* @param int $elementId
* @param string $nodeType
* @param string|null $userId
* @return bool
*/
private function basisCheckById(int $elementId, string $nodeType, ?string &$userId): bool {
try {
$userId = $this->preCheckUserId($userId);
} catch (InternalError $e) {
$this->logger->warning('Cannot pre check the user id');
}
if ($userId === '') {
return true;
}
try {
$element = $nodeType === 'table' ? $this->tableMapper->find($elementId) : $this->viewMapper->find($elementId);
return $this->basisCheck($element, $nodeType, $userId);
} catch (DoesNotExistException|MultipleObjectsReturnedException|\Exception $e) {
$this->logger->warning('Exception occurred: ' . $e->getMessage());
}
return false;
}
/**
* @param View|Table|Context $element
* @param string|null $userId
* @return bool
*/
private function userIsElementOwner($element, ?string $userId = null, ?string $nodeType = null): bool {
if ($nodeType === 'context') {
return $element->getOwnerId() === $userId;
}
return $element->getOwnership() === $userId;
}
/**
* @param View|Table $element
* @param string $userId
* @return bool
*/
public function canChangeElementOwner($element, string $userId): bool {
return $userId === '' || $this->userIsElementOwner($element, $userId);
}
}