From bcdd607cd4ce974fa85d7fcfe5302b8b94fc7fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Mon, 24 Apr 2023 22:02:32 +0200 Subject: [PATCH 01/11] update laravel version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95fb7c60..8ce25809 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- Laravel 9.x + Laravel 10.x Latest Stable Version GitHub Actions CI status Donate From d9b7107900f938dc404810c59c092a1b8325366b Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Thu, 27 Apr 2023 20:40:27 +0700 Subject: [PATCH 02/11] Typo in PHPdoc (#1106) --- assets/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/config.php b/assets/config.php index b12cd6c4..a10ad33c 100644 --- a/assets/config.php +++ b/assets/config.php @@ -112,7 +112,7 @@ return [ * See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper */ 'root_override' => [ - // Disks whose roots should be overriden after storage_path() is suffixed. + // Disks whose roots should be overridden after storage_path() is suffixed. 'local' => '%storage_path%/app/', 'public' => '%storage_path%/app/public/', ], From e070d137458c26f65bd5c4f1bf1a03ea34fb7fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 27 Jul 2023 05:15:28 +0200 Subject: [PATCH 03/11] update support link --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6870a17c..2aab2732 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: false contact_links: - name: Support Questions & Other - url: https://github.com/stancl/tenancy/blob/3.x/SUPPORT.md + url: https://archte.ch/discord about: 'If you have a question or need help using the package.' - name: Documentation Issue url: https://github.com/stancl/tenancy-docs/issues From 395192442d4000fe901888482c7d9b98ead9bff8 Mon Sep 17 00:00:00 2001 From: tamiroh Date: Fri, 18 Aug 2023 14:40:21 +0900 Subject: [PATCH 04/11] Add use (#1103) --- src/Middleware/InitializeTenancyByRequestData.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Middleware/InitializeTenancyByRequestData.php b/src/Middleware/InitializeTenancyByRequestData.php index de75d8c5..ef2132e8 100644 --- a/src/Middleware/InitializeTenancyByRequestData.php +++ b/src/Middleware/InitializeTenancyByRequestData.php @@ -6,6 +6,7 @@ namespace Stancl\Tenancy\Middleware; use Closure; use Illuminate\Http\Request; +use Stancl\Tenancy\Contracts\TenantResolver; use Stancl\Tenancy\Resolvers\RequestDataTenantResolver; use Stancl\Tenancy\Tenancy; From 4af70d302ffcf19306cc16009bd94162f3743ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 24 Aug 2023 18:21:23 +0200 Subject: [PATCH 05/11] add extra $path validation to TenantAssetsController --- .gitignore | 1 + src/Controllers/TenantAssetsController.php | 18 +++++++++++++++++- tests/TenantAssetTest.php | 13 +++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f470ba75..c7cf933c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ coverage/ clover.xml tests/Etc/tmp/queuetest.json docker-compose.override.yml +.DS_Store diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 5549da0d..1e2014a7 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -18,7 +18,7 @@ class TenantAssetsController extends Controller public function asset($path = null) { - abort_if($path === null, 404); + $this->validatePath($path); try { return response()->file(storage_path("app/public/$path")); @@ -26,4 +26,20 @@ class TenantAssetsController extends Controller abort(404); } } + + /** + * @throws \Symfony\Component\HttpKernel\Exception\HttpException + */ + protected function validatePath(string|null $path): void + { + abort_if($path === null, 404); + + $allowedRoot = storage_path('app/public'); + + // 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. + if (! str(realpath("{$allowedRoot}/{$path}"))->startsWith($allowedRoot)) { + abort(403); + } + } } diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 703ac65e..c24767cc 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -141,4 +141,17 @@ class TenantAssetTest extends TestCase $response->assertNotFound(); } + public function test_asset_controller_returns_a_403_when_an_invalid_path_is_provided() + { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + $response = $this->get(tenant_asset('../foo.txt'), [ + 'X-Tenant' => $tenant->id, + ]); + + $response->assertForbidden(); + } } From caf2267a085dd8ff0c566b3540bbe94f7a89a4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 2 Sep 2023 03:19:37 +0200 Subject: [PATCH 06/11] reimplement TenantAssetsController::validatePath() (fixes #1143) --- src/Controllers/TenantAssetsController.php | 36 ++++++++++-- tests/TenantAssetTest.php | 68 ++++++++++++++++++++-- 2 files changed, 92 insertions(+), 12 deletions(-) diff --git a/src/Controllers/TenantAssetsController.php b/src/Controllers/TenantAssetsController.php index 1e2014a7..356c25dd 100644 --- a/src/Controllers/TenantAssetsController.php +++ b/src/Controllers/TenantAssetsController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Controllers; +use Exception; use Illuminate\Routing\Controller; use Throwable; @@ -28,18 +29,41 @@ class TenantAssetsController extends Controller } /** + * 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. + * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ protected function validatePath(string|null $path): void { - abort_if($path === null, 404); + $this->abortIf($path === null, 'Empty path'); - $allowedRoot = storage_path('app/public'); + $allowedRoot = realpath(storage_path('app/public')); - // 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. - if (! str(realpath("{$allowedRoot}/{$path}"))->startsWith($allowedRoot)) { - abort(403); + // `storage_path('app/public')` doesn't exist, so it cannot contain files + $this->abortIf($allowedRoot === false, "Storage root doesn't exist"); + + $attemptedPath = realpath("{$allowedRoot}/{$path}"); + + // User is attempting to access a nonexistent file + $this->abortIf($attemptedPath === false, 'Accessing a nonexistent file'); + + // User is attempting to access a file outside the $allowedRoot folder + $this->abortIf(! str($attemptedPath)->startsWith($allowedRoot), 'Accessing a file outside the storage root'); + } + + protected function abortIf($condition, $exceptionMessage): void + { + if ($condition) { + if (app()->runningUnitTests()) { + // Makes testing the cause of the failure in validatePath() easier + throw new Exception($exceptionMessage); + } else { + // We always use 404 to avoid leaking information about the cause of the error + // e.g. when someone is trying to access a nonexistent file outside of the allowed + // root folder, we don't want to let the user know whether such a file exists or not. + abort(404); + } } } } diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index c24767cc..8aabc0e2 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; +use Exception; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; @@ -134,24 +135,79 @@ class TenantAssetTest extends TestCase $tenant = Tenant::create(); tenancy()->initialize($tenant); - $response = $this->get(tenant_asset(null), [ + + $this->withoutExceptionHandling(); + $this->expectExceptionMessage('Empty path'); // outside tests this is a 404 + + $this->get(tenant_asset(null), [ 'X-Tenant' => $tenant->id, ]); - - $response->assertNotFound(); } - public function test_asset_controller_returns_a_403_when_an_invalid_path_is_provided() + public function test_asset_controller_returns_a_404_when_the_storage_root_doesnt_exist() { TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; $tenant = Tenant::create(); tenancy()->initialize($tenant); - $response = $this->get(tenant_asset('../foo.txt'), [ + + $storageRoot = storage_path("app/public"); + + if (is_dir($storageRoot)) { + rmdir(storage_path("app/public")); + } + + $this->withoutExceptionHandling(); + $this->expectExceptionMessage("Storage root doesn't exist"); // outside tests this is a 404 + + $this->get(tenant_asset('foo.txt'), [ 'X-Tenant' => $tenant->id, ]); + } - $response->assertForbidden(); + public function test_asset_controller_returns_a_404_when_accessing_a_nonexistent_file() + { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $storageRoot = storage_path("app/public"); + + if (! is_dir($storageRoot)) { + mkdir(storage_path("app/public"), recursive: true); + } + + $this->withoutExceptionHandling(); + $this->expectExceptionMessage("Accessing a nonexistent file"); // outside tests this is a 404 + + $this->get(tenant_asset('foo.txt'), [ + 'X-Tenant' => $tenant->id, + ]); + } + + public function test_asset_controller_returns_a_404_when_accessing_a_file_outside_the_storage_root() + { + TenantAssetsController::$tenancyMiddleware = InitializeTenancyByRequestData::class; + + $tenant = Tenant::create(); + + tenancy()->initialize($tenant); + + $storageRoot = storage_path("app/public"); + + if (! is_dir($storageRoot)) { + mkdir(storage_path("app/public"), recursive: true); + file_put_contents(storage_path('app/foo.txt'), 'bar'); + } + + $this->withoutExceptionHandling(); + $this->expectExceptionMessage('Accessing a file outside the storage root'); // outside tests this is a 404 + + $this->get(tenant_asset('../foo.txt'), [ + 'X-Tenant' => $tenant->id, + ]); } } From 85c7465aca2fb017545a5304dea0ee0006bee5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 2 Sep 2023 03:20:42 +0200 Subject: [PATCH 07/11] remove unnecessary import --- tests/TenantAssetTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/TenantAssetTest.php b/tests/TenantAssetTest.php index 8aabc0e2..2852eae2 100644 --- a/tests/TenantAssetTest.php +++ b/tests/TenantAssetTest.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Stancl\Tenancy\Tests; -use Exception; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Storage; From 0b248f937dd4799aa42ecdbd2fc73c05ddb943bb Mon Sep 17 00:00:00 2001 From: Massimo Simonini Date: Tue, 21 Nov 2023 02:25:32 +0100 Subject: [PATCH 08/11] Add step option to migrate-fresh command (#1164) --- src/Commands/MigrateFresh.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 283d70b0..b626c775 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -25,6 +25,7 @@ final class MigrateFresh extends Command parent::__construct(); $this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); + $this->addOption('--step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually.'); $this->setName('tenants:migrate-fresh'); } @@ -47,6 +48,7 @@ final class MigrateFresh extends Command $this->info('Migrating.'); $this->callSilent('tenants:migrate', [ '--tenants' => [$tenant->getTenantKey()], + '--step' => $this->option('step'), '--force' => true, ]); }); From d268a06f5d8d98bbd0aa0c6fb7a4381c0bdc5c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Thu, 18 Jan 2024 14:30:40 +0100 Subject: [PATCH 09/11] tests: assert that tenants:run runs only for the specified tenants --- tests/CommandsTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index d7da0cab..78fc7c0b 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -196,10 +196,12 @@ class CommandsTest extends TestCase { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); + $tenantId3 = Tenant::create()->getTenantKey(); Artisan::call('tenants:migrate-fresh'); $this->artisan("tenants:run foo --tenants=$tenantId1 --tenants=$tenantId2 --argument='a=foo' --option='b=bar' --option='c=xyz'") ->expectsOutput('Tenant: ' . $tenantId1) - ->expectsOutput('Tenant: ' . $tenantId2); + ->expectsOutput('Tenant: ' . $tenantId2) + ->doesntExpectOutput('Tenant: ' . $tenantId3); } } From 5fe8825f13e2141c4388ae2afec01bd85f2f91ae Mon Sep 17 00:00:00 2001 From: chillbram Date: Thu, 25 Jan 2024 22:34:47 +0100 Subject: [PATCH 10/11] Make universal routes work for controller middleware (#1151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make universal routes work for controller middleware * add a fallback --------- Co-authored-by: chillbram <7299762+chillbram@users.noreply.github.com> Co-authored-by: Samuel Ć tancl --- src/Features/UniversalRoutes.php | 2 +- tests/UniversalRouteTest.php | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Features/UniversalRoutes.php b/src/Features/UniversalRoutes.php index 6b729962..40acbeae 100644 --- a/src/Features/UniversalRoutes.php +++ b/src/Features/UniversalRoutes.php @@ -35,7 +35,7 @@ class UniversalRoutes implements Feature public static function routeHasMiddleware(Route $route, $middleware): bool { - if (in_array($middleware, $route->middleware(), true)) { + if (in_array($middleware, $route->computedMiddleware ?? $route->middleware(), true)) { return true; } diff --git a/tests/UniversalRouteTest.php b/tests/UniversalRouteTest.php index c0852545..fff7b9f6 100644 --- a/tests/UniversalRouteTest.php +++ b/tests/UniversalRouteTest.php @@ -63,4 +63,46 @@ class UniversalRouteTest extends TestCase ->assertSuccessful() ->assertSee('acme'); } + + /** @test */ + public function universal_route_works_when_middleware_is_inserted_via_controller_middleware() + { + Route::middlewareGroup('universal', []); + config(['tenancy.features' => [UniversalRoutes::class]]); + + Route::get('/foo', [UniversalRouteController::class, 'show']); + + $this->get('http://localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is not initialized.'); + + $tenant = Tenant::create([ + 'id' => 'acme', + ]); + $tenant->domains()->create([ + 'domain' => 'acme.localhost', + ]); + + $this->get('http://acme.localhost/foo') + ->assertSuccessful() + ->assertSee('Tenancy is initialized.'); + } +} + +class UniversalRouteController +{ + public function getMiddleware() + { + return array_map(fn($middleware) => [ + 'middleware' => $middleware, + 'options' => [], + ], ['universal', InitializeTenancyByDomain::class]); + } + + public function show() + { + return tenancy()->initialized + ? 'Tenancy is initialized.' + : 'Tenancy is not initialized.'; + } } From 8db27a358ef71e01362dd388e5c0439fcda44944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 27 Jan 2024 22:55:59 +0100 Subject: [PATCH 11/11] Forget tenant parameter when a tenant is resolved from cache in PathTenantResolver (fix #1174) --- src/Resolvers/PathTenantResolver.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Resolvers/PathTenantResolver.php b/src/Resolvers/PathTenantResolver.php index 0b79626f..e3c32cc7 100644 --- a/src/Resolvers/PathTenantResolver.php +++ b/src/Resolvers/PathTenantResolver.php @@ -37,6 +37,14 @@ class PathTenantResolver extends Contracts\CachedTenantResolver throw new TenantCouldNotBeIdentifiedByPathException($id); } + public function resolved(Tenant $tenant, ...$args): void + { + /** @var Route $route */ + $route = $args[0]; + + $route->forgetParameter(static::$tenantParameterName); + } + public function getArgsForTenant(Tenant $tenant): array { return [