From b771fd13a983b5793bb5af952ff98991106c1c68 Mon Sep 17 00:00:00 2001 From: lukinovec Date: Tue, 18 Aug 2026 14:19:40 +0200 Subject: [PATCH] Make the disk used for serving tenant assets configurable TenantAssetController::$publicDisk is null by default, which keeps serving the assets from app/public inside the tenant's storage directory. Setting it to a disk name serves the assets from that disk's root instead. A disk with no root path throws instead of resolving to an empty path. realpath('') returns the current working directory, so the controller would end up treating the whole app directory as the allowed root. --- src/Controllers/TenantAssetController.php | 25 ++++++++++++-- tests/TenantAssetTest.php | 42 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/Controllers/TenantAssetController.php b/src/Controllers/TenantAssetController.php index bd27e7be..1863c975 100644 --- a/src/Controllers/TenantAssetController.php +++ b/src/Controllers/TenantAssetController.php @@ -29,6 +29,13 @@ class TenantAssetController implements HasMiddleware */ public static array $middleware = []; + /** + * Disk the assets are served from. + * + * When null, the assets are served from app/public inside the tenant's storage directory. + */ + public static string|null $publicDisk = null; + public static function middleware() { return array_map( @@ -59,13 +66,25 @@ class TenantAssetController implements HasMiddleware } /** - * Assets are served from app/public inside the tenant's storage directory. + * 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. * - * The directory is resolved using the FilesystemTenancyBootstrapper (rather than storage_path(), - * so that it's tenant-scoped regardless of the suffix_storage_path config). + * The storage directory is resolved using the FilesystemTenancyBootstrapper (rather than + * storage_path(), so that it's tenant-scoped regardless of the suffix_storage_path config). */ protected function assetRoot(): string { + if (static::$publicDisk) { + $diskRoot = config('filesystems.disks.' . static::$publicDisk . '.root'); + + if (! is_string($diskRoot)) { + // A disk with no root path would let the controller serve any file in the app + throw new Exception('Disk [' . static::$publicDisk . '] has no root path configured.'); + } + + return rtrim($diskRoot, '/'); + } + if ($tenant = tenant()) { return FilesystemTenancyBootstrapper::getBoundTenantStoragePath($tenant) . '/app/public'; } diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 4e6e550b..7f1b9ab0 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -30,6 +30,7 @@ beforeEach(function () { TenancyUrlGenerator::$prefixRouteNames = false; TenancyUrlGenerator::$passTenantParameterToRoutes = true; TenantAssetController::$headers = []; + TenantAssetController::$publicDisk = null; /** @var CloneRoutesAsTenant $cloneAction */ $cloneAction = app(CloneRoutesAsTenant::class); @@ -88,6 +89,47 @@ test('tenant assets are served even when the suffix_storage_path config is set t ->toBe("$centralStoragePath/tenant{$tenant->id}/app/public/$filename"); }); +test('the disk used for serving tenant assets is configurable', function () { + config([ + 'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class, + // This is tenancy's default override for the local disk's root, set it here for clarity + 'tenancy.filesystem.root_override.local' => '%storage_path%/app/', + ]); + + // The local disk's root is overridden to '%storage_path%/app/' (so it does not use 'app/public') + TenantAssetController::$publicDisk = 'local'; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $filename = 'testfile' . Str::random(8); + Storage::disk('local')->put($filename, 'bar'); + $path = Storage::disk('local')->path($filename); + + $response = pest()->get(tenant_asset($filename), ['X-Tenant' => $tenant->id]); + + // The asset is served from the disk's root instead of 'app/public' + $response->assertSuccessful(); + expect($response->getFile()->getPathname())->toBe($path); +}); + +test('tenant asset controller throws when the configured disk has no root', function () { + config([ + 'tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class, + 'filesystems.disks.rootless' => ['driver' => 's3'], + ]); + + TenantAssetController::$publicDisk = 'rootless'; + + $tenant = Tenant::create(); + tenancy()->initialize($tenant); + + $this->withoutExceptionHandling(); + pest()->expectExceptionMessage('Disk [rootless] has no root path configured.'); + + pest()->get(tenant_asset('foo.txt'), ['X-Tenant' => $tenant->id]); +}); + test('asset helper returns a link to tenant asset controller when asset url is null', function () { config(['app.asset_url' => null]); config(['tenancy.filesystem.asset_helper_override' => true]);