f7cloud_client/apps/groupfolders/lib/DAV/ACLPlugin.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

261 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\GroupFolders\DAV;
use OCA\DAV\Connector\Sabre\Node;
use OCA\GroupFolders\ACL\ACLManagerFactory;
use OCA\GroupFolders\ACL\Rule;
use OCA\GroupFolders\ACL\RuleManager;
use OCA\GroupFolders\ACL\UserMapping\IUserMapping;
use OCA\GroupFolders\Folder\FolderManager;
use OCA\GroupFolders\Mount\GroupMountPoint;
use OCP\Constants;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Log\Audit\CriticalActionPerformedEvent;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\Xml\Reader;
class ACLPlugin extends ServerPlugin {
public const ACL_ENABLED = '{http://f7cloud.org/ns}acl-enabled';
public const ACL_CAN_MANAGE = '{http://f7cloud.org/ns}acl-can-manage';
public const ACL_LIST = '{http://f7cloud.org/ns}acl-list';
public const INHERITED_ACL_LIST = '{http://f7cloud.org/ns}inherited-acl-list';
public const GROUP_FOLDER_ID = '{http://f7cloud.org/ns}group-folder-id';
private ?Server $server = null;
private ?IUser $user = null;
public function __construct(
private readonly RuleManager $ruleManager,
private readonly IUserSession $userSession,
private readonly FolderManager $folderManager,
private readonly IEventDispatcher $eventDispatcher,
private readonly ACLManagerFactory $aclManagerFactory,
private readonly IL10N $l10n,
) {
}
private function isAdmin(string $path): bool {
$folderId = $this->folderManager->getFolderByPath($path);
if ($this->user === null) {
// Happens when sharing with a remote instance
return false;
}
return $this->folderManager->canManageACL($folderId, $this->user);
}
public function initialize(Server $server): void {
$this->server = $server;
$this->user = $this->userSession->getUser();
$this->server->on('propFind', $this->propFind(...));
$this->server->on('propPatch', $this->propPatch(...));
$this->server->xml->elementMap[Rule::ACL] = Rule::class;
$this->server->xml->elementMap[self::ACL_LIST] = fn (Reader $reader): array => \Sabre\Xml\Deserializer\repeatingElements($reader, Rule::ACL);
}
/**
* @return string[]
*/
private function getParents(string $path): array {
$paths = [];
while ($path !== '') {
$path = dirname($path);
if ($path === '.' || $path === '/') {
$path = '';
}
$paths[] = $path;
}
return $paths;
}
public function propFind(PropFind $propFind, INode $node): void {
if (!$node instanceof Node) {
return;
}
$fileInfo = $node->getFileInfo();
$mount = $fileInfo->getMountPoint();
if (!$mount instanceof GroupMountPoint) {
return;
}
$propFind->handle(self::ACL_LIST, function () use ($fileInfo, $mount): ?array {
$path = trim($mount->getSourcePath() . '/' . $fileInfo->getInternalPath(), '/');
if ($this->isAdmin($fileInfo->getPath())) {
$rules = $this->ruleManager->getAllRulesForPaths($mount->getNumericStorageId(), [$path]);
} else {
if ($this->user === null) {
return [];
}
$rules = $this->ruleManager->getRulesForFilesByPath($this->user, $mount->getNumericStorageId(), [$path]);
}
return array_pop($rules);
});
$propFind->handle(self::INHERITED_ACL_LIST, function () use ($fileInfo, $mount): array {
$parentInternalPaths = $this->getParents($fileInfo->getInternalPath());
$parentPaths = array_map(fn (string $internalPath): string => trim($mount->getSourcePath() . '/' . $internalPath, '/'), $parentInternalPaths);
if ($this->isAdmin($fileInfo->getPath())) {
$rulesByPath = $this->ruleManager->getAllRulesForPaths($mount->getNumericStorageId(), $parentPaths);
} else {
if ($this->user === null) {
return [];
}
$rulesByPath = $this->ruleManager->getRulesForFilesByPath($this->user, $mount->getNumericStorageId(), $parentPaths);
}
ksort($rulesByPath);
$inheritedPermissionsByMapping = [];
$inheritedMaskByMapping = [];
$mappings = [];
foreach ($rulesByPath as $rules) {
foreach ($rules as $rule) {
$mappingKey = $rule->getUserMapping()->getType() . '::' . $rule->getUserMapping()->getId();
if (!isset($mappings[$mappingKey])) {
$mappings[$mappingKey] = $rule->getUserMapping();
}
if (!isset($inheritedPermissionsByMapping[$mappingKey])) {
$inheritedPermissionsByMapping[$mappingKey] = Constants::PERMISSION_ALL;
}
if (!isset($inheritedMaskByMapping[$mappingKey])) {
$inheritedMaskByMapping[$mappingKey] = 0;
}
$inheritedPermissionsByMapping[$mappingKey] = $rule->applyPermissions($inheritedPermissionsByMapping[$mappingKey]);
$inheritedMaskByMapping[$mappingKey] |= $rule->getMask();
}
}
return array_map(fn (IUserMapping $mapping, int $permissions, int $mask): Rule => new Rule(
$mapping,
$fileInfo->getId(),
$mask,
$permissions
), $mappings, $inheritedPermissionsByMapping, $inheritedMaskByMapping);
});
$propFind->handle(self::GROUP_FOLDER_ID, fn (): int => $this->folderManager->getFolderByPath($fileInfo->getPath()));
$propFind->handle(self::ACL_ENABLED, function () use ($fileInfo): bool {
$folderId = $this->folderManager->getFolderByPath($fileInfo->getPath());
return $this->folderManager->getFolderAclEnabled($folderId);
});
$propFind->handle(self::ACL_CAN_MANAGE, fn (): bool => $this->isAdmin($fileInfo->getPath()));
}
public function propPatch(string $path, PropPatch $propPatch): void {
if ($this->server === null) {
return;
}
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof Node) {
return;
}
$fileInfo = $node->getFileInfo();
$mount = $fileInfo->getMountPoint();
if (!$mount instanceof GroupMountPoint || !$this->isAdmin($fileInfo->getPath())) {
return;
}
// Mapping the old property to the new property.
$propPatch->handle(self::ACL_LIST, function (array $rawRules) use ($path): bool {
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof Node) {
return false;
}
$fileInfo = $node->getFileInfo();
$mount = $fileInfo->getMountPoint();
if (!$mount instanceof GroupMountPoint) {
return false;
}
if ($this->user === null) {
return false;
}
$path = trim($mount->getSourcePath() . '/' . $fileInfo->getInternalPath(), '/');
// populate fileid in rules
$rules = array_values(array_map(fn (Rule $rule): Rule => new Rule(
$rule->getUserMapping(),
$fileInfo->getId(),
$rule->getMask(),
$rule->getPermissions()
), $rawRules));
$formattedRules = array_map(fn (Rule $rule): string => $rule->getUserMapping()->getType() . ' ' . $rule->getUserMapping()->getDisplayName() . ': ' . $rule->formatPermissions(), $rules);
if (count($formattedRules)) {
$formattedRules = implode(', ', $formattedRules);
$this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('The advanced permissions for "%s" in Team folder with ID %d was set to "%s"', [
$fileInfo->getInternalPath(),
$mount->getFolderId(),
$formattedRules,
]));
} else {
$this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent('The advanced permissions for "%s" in Team folder with ID %d was cleared', [
$fileInfo->getInternalPath(),
$mount->getFolderId(),
]));
}
$aclManager = $this->aclManagerFactory->getACLManager($this->user);
$newPermissions = $aclManager->testACLPermissionsForPath($mount->getNumericStorageId(), $path, $rules);
if (!($newPermissions & Constants::PERMISSION_READ)) {
throw new BadRequest($this->l10n->t('You cannot remove your own read permission.'));
}
$existingRules = array_reduce(
$this->ruleManager->getAllRulesForPaths($mount->getNumericStorageId(), [$path]),
fn (array $rules, array $rulesForPath): array => array_merge($rules, $rulesForPath),
[]
);
$deletedRules = array_udiff($existingRules, $rules, fn (Rule $obj_a, Rule $obj_b): int => (
$obj_a->getUserMapping()->getType() === $obj_b->getUserMapping()->getType()
&& $obj_a->getUserMapping()->getId() === $obj_b->getUserMapping()->getId()
) ? 0 : -1);
foreach ($deletedRules as $deletedRule) {
$this->ruleManager->deleteRule($deletedRule);
}
foreach ($rules as $rule) {
$this->ruleManager->saveRule($rule);
}
$node->getNode()->getStorage()->getPropagator()->propagateChange($fileInfo->getInternalPath(), $fileInfo->getMtime());
return true;
});
}
}