mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 20:54:04 +00:00
Merge remote-tracking branch 'origin/3.x'
This commit is contained in:
commit
7c29764d81
10 changed files with 165 additions and 4 deletions
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Support Questions & Other
|
- 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.'
|
about: 'If you have a question or need help using the package.'
|
||||||
- name: Documentation Issue
|
- name: Documentation Issue
|
||||||
url: https://github.com/stancl/tenancy-docs/issues
|
url: https://github.com/stancl/tenancy-docs/issues
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,3 +13,4 @@ tenant-schema-test.dump
|
||||||
tests/Etc/tmp/queuetest.json
|
tests/Etc/tmp/queuetest.json
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
.php-cs-fixer.cache
|
.php-cs-fixer.cache
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://laravel.com"><img alt="Laravel 9.x" src="https://img.shields.io/badge/laravel-9.x-red.svg"></a>
|
<a href="https://laravel.com"><img alt="Laravel 10.x" src="https://img.shields.io/badge/laravel-10.x-red.svg"></a>
|
||||||
<a href="https://packagist.org/packages/stancl/tenancy"><img alt="Latest Stable Version" src="https://poser.pugx.org/stancl/tenancy/version"></a>
|
<a href="https://packagist.org/packages/stancl/tenancy"><img alt="Latest Stable Version" src="https://poser.pugx.org/stancl/tenancy/version"></a>
|
||||||
<a href="https://github.com/stancl/tenancy/actions"><img alt="GitHub Actions CI status" src="https://github.com/stancl/tenancy/workflows/CI/badge.svg"></a>
|
<a href="https://github.com/stancl/tenancy/actions"><img alt="GitHub Actions CI status" src="https://github.com/stancl/tenancy/workflows/CI/badge.svg"></a>
|
||||||
<a href="https://github.com/stancl/tenancy/blob/3.x/DONATIONS.md"><img alt="Donate" src="https://img.shields.io/badge/Donate-%3C3-red"></a>
|
<a href="https://github.com/stancl/tenancy/blob/3.x/DONATIONS.md"><img alt="Donate" src="https://img.shields.io/badge/Donate-%3C3-red"></a>
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ return [
|
||||||
* See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper
|
* See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper
|
||||||
*/
|
*/
|
||||||
'root_override' => [
|
'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/',
|
'local' => '%storage_path%/app/',
|
||||||
'public' => '%storage_path%/app/public/',
|
'public' => '%storage_path%/app/public/',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class MigrateFresh extends BaseCommand
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->addOption('--drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null);
|
$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');
|
$this->setName('tenants:migrate-fresh');
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +41,7 @@ class MigrateFresh extends BaseCommand
|
||||||
$this->components->task('Migrating', function () use ($tenant) {
|
$this->components->task('Migrating', function () use ($tenant) {
|
||||||
$this->callSilent('tenants:migrate', [
|
$this->callSilent('tenants:migrate', [
|
||||||
'--tenants' => [$tenant->getTenantKey()],
|
'--tenants' => [$tenant->getTenantKey()],
|
||||||
|
'--step' => $this->option('step'),
|
||||||
'--force' => true,
|
'--force' => true,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ class TenantAssetController implements HasMiddleware // todo@docs this was renam
|
||||||
*/
|
*/
|
||||||
public function __invoke(string $path = null): BinaryFileResponse
|
public function __invoke(string $path = null): BinaryFileResponse
|
||||||
{
|
{
|
||||||
abort_if($path === null, 404);
|
$this->validatePath($path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return response()->file(storage_path("app/public/$path"));
|
return response()->file(storage_path("app/public/$path"));
|
||||||
|
|
@ -31,4 +31,43 @@ class TenantAssetController implements HasMiddleware // todo@docs this was renam
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
{
|
||||||
|
$this->abortIf($path === null, 'Empty path');
|
||||||
|
|
||||||
|
$allowedRoot = realpath(storage_path('app/public'));
|
||||||
|
|
||||||
|
// `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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,14 @@ class PathTenantResolver extends Contracts\CachedTenantResolver
|
||||||
throw new TenantCouldNotBeIdentifiedByPathException($id);
|
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
|
public function getArgsForTenant(Tenant $tenant): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -295,11 +295,13 @@ test('migrate fresh command works', function () {
|
||||||
test('run command with array of tenants works', function () {
|
test('run command with array of tenants works', function () {
|
||||||
$tenantId1 = Tenant::create()->getTenantKey();
|
$tenantId1 = Tenant::create()->getTenantKey();
|
||||||
$tenantId2 = Tenant::create()->getTenantKey();
|
$tenantId2 = Tenant::create()->getTenantKey();
|
||||||
|
$tenantId3 = Tenant::create()->getTenantKey();
|
||||||
Artisan::call('tenants:migrate-fresh');
|
Artisan::call('tenants:migrate-fresh');
|
||||||
|
|
||||||
pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'")
|
pest()->artisan("tenants:run --tenants=$tenantId1 --tenants=$tenantId2 'foo foo --b=bar --c=xyz'")
|
||||||
->expectsOutputToContain('Tenant: ' . $tenantId1)
|
->expectsOutputToContain('Tenant: ' . $tenantId1)
|
||||||
->expectsOutputToContain('Tenant: ' . $tenantId2)
|
->expectsOutputToContain('Tenant: ' . $tenantId2)
|
||||||
|
->doesntExpectOutput('Tenant: ' . $tenantId3)
|
||||||
->assertExitCode(0);
|
->assertExitCode(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,11 +145,78 @@ test('test asset controller returns a 404 when no path is provided', function ()
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
pest()->expectExceptionMessage('Empty path'); // outside tests this is a 404
|
||||||
|
|
||||||
pest()->get(tenant_asset(null), [
|
pest()->get(tenant_asset(null), [
|
||||||
'X-Tenant' => $tenant->id,
|
'X-Tenant' => $tenant->id,
|
||||||
])->assertNotFound();
|
])->assertNotFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tenant asset controller returns a 404 when the storage root doesnt exist', function () {
|
||||||
|
config(['tenancy.identification.default_middleware' => InitializeTenancyByRequestData::class]);
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
$storageRoot = storage_path("app/public");
|
||||||
|
|
||||||
|
if (is_dir($storageRoot)) {
|
||||||
|
rmdir(storage_path("app/public"));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
pest()->expectExceptionMessage("Storage root doesn't exist"); // outside tests this is a 404
|
||||||
|
|
||||||
|
pest()->get(tenant_asset('foo.txt'), [
|
||||||
|
'X-Tenant' => $tenant->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tenant asset controller returns a 404 when accessing a nonexistent file', function () {
|
||||||
|
config(['tenancy.identification.default_middleware' => 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();
|
||||||
|
pest()->expectExceptionMessage("Accessing a nonexistent file"); // outside tests this is a 404
|
||||||
|
|
||||||
|
pest()->get(tenant_asset('foo.txt'), [
|
||||||
|
'X-Tenant' => $tenant->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test asset controller returns a 404 when accessing a file outside the storage root', function () {
|
||||||
|
config(['tenancy.identification.default_middleware' => 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();
|
||||||
|
pest()->expectExceptionMessage('Accessing a file outside the storage root'); // outside tests this is a 404
|
||||||
|
|
||||||
|
pest()->get(tenant_asset('../foo.txt'), [
|
||||||
|
'X-Tenant' => $tenant->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
function getEnvironmentSetUp($app)
|
function getEnvironmentSetUp($app)
|
||||||
{
|
{
|
||||||
$app->booted(function () {
|
$app->booted(function () {
|
||||||
|
|
|
||||||
|
|
@ -417,4 +417,46 @@ class Controller extends BaseController
|
||||||
{
|
{
|
||||||
return tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.';
|
return tenant() ? 'Tenancy is initialized.' : 'Tenancy is not initialized.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @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.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue