Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat(files): Add appconfig value to disable fixed userfolder permissi…
…ons optimization

Signed-off-by: Robin Appelman <[email protected]>
Signed-off-by: Louis Chemineau <[email protected]>
  • Loading branch information
artonge committed Sep 26, 2025
commit bd8a0ec3cfd0af9f6d7183d7753ea52ef772c1df
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php',
'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php',
'OCA\\Files\\Command\\WindowsCompatibleFilenames' => $baseDir . '/../lib/Command/WindowsCompatibleFilenames.php',
'OCA\\Files\\ConfigLexicon' => $baseDir . '/../lib/ConfigLexicon.php',
'OCA\\Files\\Controller\\ApiController' => $baseDir . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\ConversionApiController' => $baseDir . '/../lib/Controller/ConversionApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php',
Expand Down
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php',
'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php',
'OCA\\Files\\Command\\WindowsCompatibleFilenames' => __DIR__ . '/..' . '/../lib/Command/WindowsCompatibleFilenames.php',
'OCA\\Files\\ConfigLexicon' => __DIR__ . '/..' . '/../lib/ConfigLexicon.php',
'OCA\\Files\\Controller\\ApiController' => __DIR__ . '/..' . '/../lib/Controller/ApiController.php',
'OCA\\Files\\Controller\\ConversionApiController' => __DIR__ . '/..' . '/../lib/Controller/ConversionApiController.php',
'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php',
Expand Down
4 changes: 4 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use OCA\Files\Capabilities;
use OCA\Files\Collaboration\Resources\Listener;
use OCA\Files\Collaboration\Resources\ResourceProvider;
use OCA\Files\ConfigLexicon;
use OCA\Files\Controller\ApiController;
use OCA\Files\Dashboard\FavoriteWidget;
use OCA\Files\DirectEditingCapabilities;
Expand Down Expand Up @@ -124,6 +125,9 @@ public function register(IRegistrationContext $context): void {

$context->registerNotifierService(Notifier::class);
$context->registerDashboardWidget(FavoriteWidget::class);

$context->registerConfigLexicon(ConfigLexicon::class);

}

public function boot(IBootContext $context): void {
Expand Down
46 changes: 46 additions & 0 deletions apps/files/lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files;

use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;

/**
* Config Lexicon for files.
*
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
*
* {@see ILexicon}
*/
class ConfigLexicon implements ILexicon {
public const OVERWRITES_HOME_FOLDERS = 'overwrites_home_folders';

public function getStrictness(): Strictness {
return Strictness::NOTICE;
}

public function getAppConfigs(): array {
return [
new Entry(
self::OVERWRITES_HOME_FOLDERS,
ValueType::ARRAY,
defaultRaw: [],
definition: 'List of applications overwriting home folders',
lazy: false,
note: 'It will be populated with app IDs of mount providers that overwrite home folders. Currently, only files_external and groupfolders.',
),
];
}

public function getUserConfigs(): array {
return [];
}
}
64 changes: 38 additions & 26 deletions lib/private/Files/Node/LazyUserFolder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,50 @@
use Psr\Log\LoggerInterface;

class LazyUserFolder extends LazyFolder {
private IUser $user;
private string $path;
private IMountManager $mountManager;

public function __construct(IRootFolder $rootFolder, IUser $user, IMountManager $mountManager) {
$this->user = $user;
$this->mountManager = $mountManager;
public function __construct(
IRootFolder $rootFolder,
private IUser $user,
private IMountManager $mountManager,
bool $useDefaultHomeFoldersPermissions = true,
) {
$this->path = '/' . $user->getUID() . '/files';
parent::__construct($rootFolder, function () use ($user): Folder {
try {
$node = $this->getRootFolder()->get($this->path);
if ($node instanceof File) {
$e = new \RuntimeException();
\OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [
'exception' => $e,
]);
throw $e;
}
return $node;
} catch (NotFoundException $e) {
if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) {
$this->getRootFolder()->newFolder('/' . $user->getUID());
}
return $this->getRootFolder()->newFolder($this->path);
}
}, [
$data = [
'path' => $this->path,
// Sharing user root folder is not allowed
'permissions' => Constants::PERMISSION_ALL ^ Constants::PERMISSION_SHARE,
'type' => FileInfo::TYPE_FOLDER,
'mimetype' => FileInfo::MIMETYPE_FOLDER,
]);
];

// By default, we assume the permissions for the users' home folders.
// If a mount point is mounted on a user's home folder, the permissions cannot be assumed.
if ($useDefaultHomeFoldersPermissions) {
// Sharing user root folder is not allowed
$data['permissions'] = Constants::PERMISSION_ALL ^ Constants::PERMISSION_SHARE;
}

parent::__construct(
$rootFolder,
function () use ($user): Folder {
try {
$node = $this->getRootFolder()->get($this->path);
if ($node instanceof File) {
$e = new \RuntimeException();
\OCP\Server::get(LoggerInterface::class)->error('User root storage is not a folder: ' . $this->path, [
'exception' => $e,
]);
throw $e;
}
return $node;
} catch (NotFoundException $e) {
if (!$this->getRootFolder()->nodeExists('/' . $user->getUID())) {
$this->getRootFolder()->newFolder('/' . $user->getUID());
}
return $this->getRootFolder()->newFolder($this->path);
}
},
$data,
);
}

public function getMountPoint() {
Expand Down
36 changes: 13 additions & 23 deletions lib/private/Files/Node/Root.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use OC\Files\View;
use OC\Hooks\PublicEmitter;
use OC\User\NoUserException;
use OCA\Files\AppInfo\Application;
use OCA\Files\ConfigLexicon;
use OCP\Cache\CappedMemoryCache;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
Expand All @@ -24,6 +26,7 @@
use OCP\Files\Node as INode;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IAppConfig;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUser;
Expand Down Expand Up @@ -51,43 +54,30 @@
* @package OC\Files\Node
*/
class Root extends Folder implements IRootFolder {
private Manager $mountManager;
private PublicEmitter $emitter;
private ?IUser $user;
private CappedMemoryCache $userFolderCache;
private IUserMountCache $userMountCache;
private LoggerInterface $logger;
private IUserManager $userManager;
private IEventDispatcher $eventDispatcher;
private ICache $pathByIdCache;
private bool $useDefaultHomeFoldersPermissions = true;

/**
* @param Manager $manager
* @param View $view
* @param IUser|null $user
*/
public function __construct(
$manager,
$view,
$user,
IUserMountCache $userMountCache,
LoggerInterface $logger,
IUserManager $userManager,
private Manager $mountManager,
View $view,
private ?IUser $user,
private IUserMountCache $userMountCache,
private LoggerInterface $logger,
private IUserManager $userManager,
IEventDispatcher $eventDispatcher,
ICacheFactory $cacheFactory,
IAppConfig $appConfig,
) {
parent::__construct($this, $view, '');
$this->mountManager = $manager;
$this->user = $user;
$this->emitter = new PublicEmitter();
$this->userFolderCache = new CappedMemoryCache();
$this->userMountCache = $userMountCache;
$this->logger = $logger;
$this->userManager = $userManager;
$eventDispatcher->addListener(FilesystemTornDownEvent::class, function () {
$this->userFolderCache = new CappedMemoryCache();
});
$this->pathByIdCache = $cacheFactory->createLocal('path-by-id');
$this->useDefaultHomeFoldersPermissions = count($appConfig->getValueArray(Application::APP_ID, ConfigLexicon::OVERWRITES_HOME_FOLDERS)) === 0;
}

/**
Expand Down Expand Up @@ -367,7 +357,7 @@ public function getUserFolder($userId) {
$folder = $this->newFolder('/' . $userId . '/files');
}
} else {
$folder = new LazyUserFolder($this, $userObject, $this->mountManager);
$folder = new LazyUserFolder($this, $userObject, $this->mountManager, $this->useDefaultHomeFoldersPermissions);
}

$this->userFolderCache->set($userId, $folder);
Expand Down
1 change: 1 addition & 0 deletions lib/private/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ public function __construct($webRoot, \OC\Config $config) {
$this->get(IUserManager::class),
$this->get(IEventDispatcher::class),
$this->get(ICacheFactory::class),
$this->get(IAppConfig::class),
);

$previewConnector = new \OC\Preview\WatcherConnector(
Expand Down
15 changes: 10 additions & 5 deletions tests/lib/Files/Node/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected function getViewDeleteMethod() {
public function testGetContent(): void {
/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
$root = $this->getMockBuilder(Root::class)
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig])
->getMock();

$hook = function ($file): void {
Expand Down Expand Up @@ -74,7 +74,7 @@ public function testGetContentNotPermitted(): void {

/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
$root = $this->getMockBuilder(Root::class)
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig])
->getMock();

$root->expects($this->any())
Expand All @@ -93,7 +93,7 @@ public function testGetContentNotPermitted(): void {
public function testPutContent(): void {
/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
$root = $this->getMockBuilder(Root::class)
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig])
->getMock();

$root->expects($this->any())
Expand All @@ -120,7 +120,7 @@ public function testPutContentNotPermitted(): void {

/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
$root = $this->getMockBuilder(Root::class)
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig])
->getMock();

$this->view->expects($this->once())
Expand All @@ -135,7 +135,7 @@ public function testPutContentNotPermitted(): void {
public function testGetMimeType(): void {
/** @var \OC\Files\Node\Root|\PHPUnit\Framework\MockObject\MockObject $root */
$root = $this->getMockBuilder(Root::class)
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory])
->setConstructorArgs([$this->manager, $this->view, $this->user, $this->userMountCache, $this->logger, $this->userManager, $this->eventDispatcher, $this->cacheFactory, $this->appConfig])
->getMock();

$this->view->expects($this->once())
Expand All @@ -161,6 +161,7 @@ public function testFOpenRead(): void {
$this->userManager,
$this->eventDispatcher,
$this->cacheFactory,
$this->appConfig,
);

$hook = function ($file): void {
Expand Down Expand Up @@ -198,6 +199,7 @@ public function testFOpenWrite(): void {
$this->userManager,
$this->eventDispatcher,
$this->cacheFactory,
$this->appConfig,
);
$hooksCalled = 0;
$hook = function ($file) use (&$hooksCalled): void {
Expand Down Expand Up @@ -239,6 +241,7 @@ public function testFOpenReadNotPermitted(): void {
$this->userManager,
$this->eventDispatcher,
$this->cacheFactory,
$this->appConfig,
);
$hook = function ($file): void {
throw new \Exception('Hooks are not supposed to be called');
Expand Down Expand Up @@ -266,6 +269,7 @@ public function testFOpenReadWriteNoReadPermissions(): void {
$this->userManager,
$this->eventDispatcher,
$this->cacheFactory,
$this->appConfig,
);
$hook = function (): void {
throw new \Exception('Hooks are not supposed to be called');
Expand Down Expand Up @@ -293,6 +297,7 @@ public function testFOpenReadWriteNoWritePermissions(): void {
$this->userManager,
$this->eventDispatcher,
$this->cacheFactory,
$this->appConfig,
);
$hook = function (): void {
throw new \Exception('Hooks are not supposed to be called');
Expand Down
Loading