From e0e696f83ead968d3e75ec261b3f0dc9a296ed29 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Wed, 2 Sep 2026 14:42:23 +0200 Subject: [PATCH] Throw an exception in TenantAssetController if the disk is not tenant-aware Instead of just saying that the publicDisk *should* be listed in tenancy.filesystem.disks, enforce that -- if the disk isn't tenant-aware, throw an exception. Also update comments accordingly. E.g. since scoped disks don't have to have a single "parent disk" (the parent can also be a scoped disk and have another parent, and so on), use "base disk". --- src/Controllers/TenantAssetController.php | 45 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/Controllers/TenantAssetController.php b/src/Controllers/TenantAssetController.php index 8e21ab45..8ba466bd 100644 --- a/src/Controllers/TenantAssetController.php +++ b/src/Controllers/TenantAssetController.php @@ -47,10 +47,10 @@ class TenantAssetController implements HasMiddleware * When null, the assets are served from app/public inside the tenant's storage directory. * * The disk has to be local, since the assets are read from the filesystem. Disks using the - * 'scoped' driver are supported as long as their parent disk uses the 'local' driver. + * 'scoped' driver are supported as long as the disk they're based on uses the 'local' driver. * - * It should also be listed in tenancy.filesystem.disks -- for scoped disks, it's the parent - * disk that has to be listed there (since a scoped disk inherits the parent's root). + * The disk also has to be listed in tenancy.filesystem.disks -- for scoped disks, it's the + * disk they're based on that has to be listed there (since a scoped disk inherits its root). * FilesystemTenancyBootstrapper only scopes the roots of disks listed there, so * without that every tenant would be served the same (central) directory. */ @@ -87,8 +87,8 @@ class TenantAssetController implements HasMiddleware /** * Directory the assets are served from -- the root of the $publicDisk, or app/public - * inside the tenant's storage directory when no disk is configured. With no current - * tenant (e.g. on a universal route), the central storage directory is used. + * inside the tenant's storage directory when no disk is configured. With no disk and + * no current tenant (e.g. on a universal route), the central app/public is used. * * The tenant's storage directory is resolved using the FilesystemTenancyBootstrapper (rather * than storage_path(), so that it's tenant-scoped regardless of the suffix_storage_path config). @@ -99,13 +99,20 @@ class TenantAssetController implements HasMiddleware $disk = Storage::disk(static::$publicDisk); if (! $disk instanceof LocalFilesystemAdapter) { - // The root has to be read from the resolved disk rather than from the disk's config, - // since the config root isn't the full root path of every local disk. Disks using the - // 'scoped' driver have no root in their config -- they inherit their parent disk's root -- - // and a 'prefix' is part of the root path as well. throw new Exception('Disk [' . static::$publicDisk . '] is not a local disk. Only local disks can be used for serving assets.'); } + $baseDiskName = $this->baseDiskName(static::$publicDisk); + + if (! in_array($baseDiskName, config('tenancy.filesystem.disks'), true)) { + // FilesystemTenancyBootstrapper only scopes the roots of disks listed in tenancy.filesystem.disks. + // Without that, the root stays central and every tenant would be served the same directory. + throw new Exception("Disk [$baseDiskName] is not tenant-aware. Add it to the tenancy.filesystem.disks config to make its root tenant-specific."); + } + + // The root is read from the resolved disk rather than from the disk's config, since the + // config root isn't the full root path of every local disk. Disks using the 'scoped' driver + // have no root in their config, and a 'prefix' is part of the root path as well. return rtrim($disk->path(''), DIRECTORY_SEPARATOR); } @@ -116,6 +123,26 @@ class TenantAssetController implements HasMiddleware return storage_path('app/public'); } + /** + * Name of the disk whose root the passed disk uses. + * + * Disks using the 'scoped' driver have no root of their own -- they inherit the root of their parent disk, + * which can be scoped as well, so the final/base parent is what has to be tenant-aware. + */ + protected function baseDiskName(string $disk): string + { + while (config("filesystems.disks.$disk.driver") === 'scoped') { + if (! is_string($parent = config("filesystems.disks.$disk.disk"))) { + // Laravel allows configuring the parent inline as an array, in which case it has no name + throw new Exception("Disk [$disk] has its parent disk configured inline. Use a named parent disk listed in tenancy.filesystem.disks."); + } + + $disk = $parent; + } + + return $disk; + } + /** * Prevent path traversal attacks. This is generally a non-issue on modern * webservers but it's still worth handling on the application level as well.