artisan('tenants:migrate')->assertExitCode(0); } public function setUp(): void { parent::setUp(); $this->artisan('migrate', [ '--path' => __DIR__ . '/../assets/2019_09_15_000020_create_tenant_user_impersonation_tokens_table.php', '--realpath' => true, ])->assertExitCode(0); config([ 'tenancy.bootstrappers' => [ DatabaseTenancyBootstrapper::class, ], 'tenancy.features' => [ UserImpersonation::class, ], ]); Event::listen( TenantCreated::class, JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) { return $event->tenant; })->toListener() ); Event::listen(TenancyInitialized::class, BootstrapTenancy::class); Event::listen(TenancyEnded::class, RevertToCentralContext::class); config(['auth.providers.users.model' => ImpersonationUser::class]); } public function makeLoginRoute() { Route::get('/login', function () { return 'Please log in'; })->name('login'); } public function getRoutes($loginRoute = true, $authGuard = 'web'): Closure { return function () use ($loginRoute, $authGuard) { if ($loginRoute) { $this->makeLoginRoute(); } Route::get('/dashboard', function () use ($authGuard) { return 'You are logged in as ' . auth()->guard($authGuard)->user()->name; })->middleware('auth:' . $authGuard); Route::get('/impersonate/{token}', function ($token) { return UserImpersonation::makeResponse($token); }); }; } /** @test */ public function tenant_user_can_be_impersonated_on_a_tenant_domain() { Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => 'foo.localhost', ]); $this->migrateTenants(); $user = $tenant->run(function () { return ImpersonationUser::create([ 'name' => 'Joe', 'email' => 'joe@local', 'password' => bcrypt('secret'), ]); }); // We try to visit the dashboard directly, before impersonating the user. $this->get('http://foo.localhost/dashboard') ->assertRedirect('http://foo.localhost/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); $this->get('http://foo.localhost/impersonate/' . $token->token) ->assertRedirect('http://foo.localhost/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. $this->get('http://foo.localhost/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); } /** @test */ public function tenant_user_can_be_impersonated_on_a_tenant_path() { $this->makeLoginRoute(); Route::middleware(InitializeTenancyByPath::class)->prefix('/{tenant}')->group($this->getRoutes(false)); $tenant = Tenant::create([ 'id' => 'acme', 'tenancy_db_name' => 'db' . Str::random(16), ]); $this->migrateTenants(); $user = $tenant->run(function () { return ImpersonationUser::create([ 'name' => 'Joe', 'email' => 'joe@local', 'password' => bcrypt('secret'), ]); }); // We try to visit the dashboard directly, before impersonating the user. $this->get('/acme/dashboard') ->assertRedirect('/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/acme/dashboard'); $this->get('/acme/impersonate/' . $token->token) ->assertRedirect('/acme/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. $this->get('/acme/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); } /** @test */ public function tokens_have_a_limited_ttl() { Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => 'foo.localhost', ]); $this->migrateTenants(); $user = $tenant->run(function () { return ImpersonationUser::create([ 'name' => 'Joe', 'email' => 'joe@local', 'password' => bcrypt('secret'), ]); }); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); $token->update([ 'created_at' => Carbon::now()->subtract(CarbonInterval::make('100s')), ]); $this->followingRedirects() ->get('http://foo.localhost/impersonate/' . $token->token) ->assertStatus(403); } /** @test */ public function tokens_are_deleted_after_use() { Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes()); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => 'foo.localhost', ]); $this->migrateTenants(); $user = $tenant->run(function () { return ImpersonationUser::create([ 'name' => 'Joe', 'email' => 'joe@local', 'password' => bcrypt('secret'), ]); }); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard'); $this->assertNotNull(ImpersonationToken::find($token->token)); $this->followingRedirects() ->get('http://foo.localhost/impersonate/' . $token->token) ->assertSuccessful() ->assertSee('You are logged in as Joe'); $this->assertNull(ImpersonationToken::find($token->token)); } /** @test */ public function impersonation_works_with_multiple_models_and_guards() { config([ 'auth.guards.another' => [ 'driver' => 'session', 'provider' => 'another_users', ], 'auth.providers.another_users' => [ 'driver' => 'eloquent', 'model' => AnotherImpersonationUser::class, ], ]); Auth::extend('another', function ($app, $name, array $config) { return new SessionGuard($name, Auth::createUserProvider($config['provider']), session()); }); Route::middleware(InitializeTenancyByDomain::class)->group($this->getRoutes(true, 'another')); $tenant = Tenant::create(); $tenant->domains()->create([ 'domain' => 'foo.localhost', ]); $this->migrateTenants(); $user = $tenant->run(function () { return AnotherImpersonationUser::create([ 'name' => 'Joe', 'email' => 'joe@local', 'password' => bcrypt('secret'), ]); }); // We try to visit the dashboard directly, before impersonating the user. $this->get('http://foo.localhost/dashboard') ->assertRedirect('http://foo.localhost/login'); // We impersonate the user $token = tenancy()->impersonate($tenant, $user->id, '/dashboard', 'another'); $this->get('http://foo.localhost/impersonate/' . $token->token) ->assertRedirect('http://foo.localhost/dashboard'); // Now we try to visit the dashboard directly, after impersonating the user. $this->get('http://foo.localhost/dashboard') ->assertSuccessful() ->assertSee('You are logged in as Joe'); Tenant::first()->run(function () { $this->assertSame('Joe', auth()->guard('another')->user()->name); $this->assertSame(null, auth()->guard('web')->user()); }); } } class ImpersonationUser extends Authenticable { protected $guarded = []; protected $table = 'users'; } class AnotherImpersonationUser extends Authenticable { protected $guarded = []; protected $table = 'users'; }