From 10484739c9329bcfae239b11346143e6a641491d Mon Sep 17 00:00:00 2001 From: Matteo Gheza Date: Tue, 6 Jun 2023 18:53:49 +0200 Subject: [PATCH] Add support for impersonation --- .../app/Http/Controllers/AuthController.php | 21 +++++ backend/app/Models/User.php | 18 ++++ .../app/Providers/EventServiceProvider.php | 13 ++- backend/composer.json | 1 + backend/composer.lock | 85 ++++++++++++++++--- backend/config/app.php | 1 + backend/config/laratrust_seeder.php | 5 +- backend/config/laravel-impersonate.php | 41 +++++++++ backend/routes/api.php | 5 +- .../_components/table/table.component.html | 3 +- .../app/_components/table/table.component.ts | 6 +- frontend/src/app/_guards/authorize.guard.ts | 2 +- .../src/app/_routes/list/list.component.ts | 4 +- frontend/src/app/_services/auth.service.ts | 60 ++++++++----- frontend/src/app/app.component.html | 4 +- frontend/src/assets/i18n/en.json | 1 + frontend/src/assets/i18n/it.json | 1 + 17 files changed, 227 insertions(+), 44 deletions(-) create mode 100644 backend/config/laravel-impersonate.php diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php index 9679548..11dbd6e 100644 --- a/backend/app/Http/Controllers/AuthController.php +++ b/backend/app/Http/Controllers/AuthController.php @@ -62,11 +62,32 @@ class AuthController extends Controller public function me(Request $request) { + $impersonateManager = app('impersonate'); return [ ...$request->user()->toArray(), "permissions" => array_map(function($p) { return $p["name"]; }, $request->user()->allPermissions()->toArray()), + "impersonating_user" => $impersonateManager->isImpersonating(), + "impersonator_id" => $impersonateManager->getImpersonatorId() ]; } + + public function impersonate(Request $request, $user) + { + $impersonatedUser = User::find($user); + $request->user()->impersonate($impersonatedUser); + $token = $impersonatedUser->createToken('auth_token')->plainTextToken; + + return response()->json([ + 'access_token' => $token, + 'token_type' => 'Bearer', + ]); + } + + public function stopImpersonating(Request $request) + { + $request->user()->leaveImpersonation(); + return; + } } diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 1f538ea..c10f7a7 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -8,10 +8,12 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; use Laratrust\Traits\LaratrustUserTrait; +use Lab404\Impersonate\Models\Impersonate; class User extends Authenticatable { use LaratrustUserTrait; + use Impersonate; use HasApiTokens, HasFactory, Notifiable; /** @@ -55,4 +57,20 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'last_access' => 'datetime', ]; + + /** + * @return bool + */ + public function canImpersonate() + { + return $this->hasPermission("users-impersonate"); + } + + /** + * @return bool + */ + public function canBeImpersonated() + { + return !$this->hasPermission("users-impersonate"); + } } diff --git a/backend/app/Providers/EventServiceProvider.php b/backend/app/Providers/EventServiceProvider.php index 2d65aac..1754aa6 100644 --- a/backend/app/Providers/EventServiceProvider.php +++ b/backend/app/Providers/EventServiceProvider.php @@ -25,7 +25,18 @@ class EventServiceProvider extends ServiceProvider */ public function boot(): void { - // + Event::listen( + \Lab404\Impersonate\Events\TakeImpersonation::class, + function (\Lab404\Impersonate\Events\TakeImpersonation $event) { + session()->put('password_hash_sanctum', $event->impersonated->getAuthPassword()); + } + ); + Event::listen( + \Lab404\Impersonate\Events\LeaveImpersonation::class, + function (\Lab404\Impersonate\Events\LeaveImpersonation $event) { + session()->put('password_hash_sanctum', $event->impersonator->getAuthPassword()); + } + ); } /** diff --git a/backend/composer.json b/backend/composer.json index fb60bc9..fc0a09a 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -7,6 +7,7 @@ "require": { "php": "^8.1", "guzzlehttp/guzzle": "^7.2", + "lab404/laravel-impersonate": "^1.7", "laravel/framework": "^10.0", "laravel/sanctum": "^3.2", "laravel/tinker": "^2.8", diff --git a/backend/composer.lock b/backend/composer.lock index ce162fc..76ce803 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b5d89cfe094ee5746cee5265ecab952d", + "content-hash": "c1b31b310ab296e893ef90608cd91a72", "packages": [ { "name": "brick/math", @@ -780,16 +780,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.4.3", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "67c26b443f348a51926030c83481b85718457d3d" + "reference": "0454e12ef0cd597ccd2adb036f7bda4e7fface66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d", - "reference": "67c26b443f348a51926030c83481b85718457d3d", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/0454e12ef0cd597ccd2adb036f7bda4e7fface66", + "reference": "0454e12ef0cd597ccd2adb036f7bda4e7fface66", "shasum": "" }, "require": { @@ -815,9 +815,6 @@ "bamarni-bin": { "bin-links": true, "forward-command": false - }, - "branch-alias": { - "dev-master": "2.4-dev" } }, "autoload": { @@ -879,7 +876,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.4.3" + "source": "https://github.com/guzzle/psr7/tree/2.4.5" }, "funding": [ { @@ -895,7 +892,7 @@ "type": "tidelift" } ], - "time": "2022-10-26T14:07:24+00:00" + "time": "2023-04-17T16:00:45+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1031,6 +1028,74 @@ }, "time": "2022-03-02T17:32:19+00:00" }, + { + "name": "lab404/laravel-impersonate", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/404labfr/laravel-impersonate.git", + "reference": "d8ab69f05daab4117b313e11ca007fbf3199a1ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/404labfr/laravel-impersonate/zipball/d8ab69f05daab4117b313e11ca007fbf3199a1ab", + "reference": "d8ab69f05daab4117b313e11ca007fbf3199a1ab", + "shasum": "" + }, + "require": { + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "php": "^7.2 | ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/database": "^4.0 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "orchestra/testbench": "^4.0 | ^5.0 | ^6.0 | ^7.0 | ^8.0", + "phpunit/phpunit": "^7.5 | ^8.0 | ^9.0 | ^10.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Lab404\\Impersonate\\ImpersonateServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Lab404\\Impersonate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marceau Casals", + "email": "marceau@casals.fr" + } + ], + "description": "Laravel Impersonate is a plugin that allows to you to authenticate as your users.", + "keywords": [ + "auth", + "impersonate", + "impersonation", + "laravel", + "laravel-package", + "laravel-plugin", + "package", + "plugin", + "user" + ], + "support": { + "issues": "https://github.com/404labfr/laravel-impersonate/issues", + "source": "https://github.com/404labfr/laravel-impersonate/tree/1.7.4" + }, + "time": "2023-01-25T16:56:05+00:00" + }, { "name": "laravel/framework", "version": "v10.0.3", diff --git a/backend/config/app.php b/backend/config/app.php index 04994fe..1f40070 100644 --- a/backend/config/app.php +++ b/backend/config/app.php @@ -185,6 +185,7 @@ return [ /* * Package Service Providers... */ + Lab404\Impersonate\ImpersonateServiceProvider::class, /* * Application Service Providers... diff --git a/backend/config/laratrust_seeder.php b/backend/config/laratrust_seeder.php index 4ca2aee..2a6ab17 100644 --- a/backend/config/laratrust_seeder.php +++ b/backend/config/laratrust_seeder.php @@ -13,7 +13,7 @@ return [ 'roles_structure' => [ 'superadmin' => [ - 'users' => 'c,r,u,d', + 'users' => 'c,r,u,d,i', ], 'admin' => [ 'users' => 'c,r,u' @@ -31,6 +31,7 @@ return [ 'lr' => 'limitedRead', 'r' => 'read', 'u' => 'update', - 'd' => 'delete' + 'd' => 'delete', + 'i' => 'impersonate' ] ]; diff --git a/backend/config/laravel-impersonate.php b/backend/config/laravel-impersonate.php new file mode 100644 index 0000000..f1aef8d --- /dev/null +++ b/backend/config/laravel-impersonate.php @@ -0,0 +1,41 @@ + 'impersonated_by', + + /** + * The session key used to stored the original user guard. + */ + 'session_guard' => 'impersonator_guard', + + /** + * The session key used to stored what guard is impersonator using. + */ + 'session_guard_using' => 'impersonator_guard_using', + + /** + * The default impersonator guard used. + */ + 'default_impersonator_guard' => 'web', + + /** + * The URI to redirect after taking an impersonation. + * + * Only used in the built-in controller. + * * Use 'back' to redirect to the previous page + */ + 'take_redirect_to' => '/', + + /** + * The URI to redirect after leaving an impersonation. + * + * Only used in the built-in controller. + * Use 'back' to redirect to the previous page + */ + 'leave_redirect_to' => '/', + +]; diff --git a/backend/routes/api.php b/backend/routes/api.php index 31e96f3..aaadf02 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -22,9 +22,12 @@ use Illuminate\Support\Facades\Artisan; Route::post('/register', [AuthController::class, 'register']); Route::post('/login', [AuthController::class, 'login']); -Route::middleware('auth:sanctum')->group( function () { +Route::middleware('auth:web')->group( function () { Route::get('/me', [AuthController::class, 'me']); Route::post('/me', [AuthController::class, 'me']); + + Route::post('/impersonate/{user}', [AuthController::class, 'impersonate']); + Route::post('/stop_impersonating', [AuthController::class, 'stopImpersonating']); Route::get('/list', [UserController::class, 'index']); diff --git a/frontend/src/app/_components/table/table.component.html b/frontend/src/app/_components/table/table.component.html index 1c626b3..6720d71 100644 --- a/frontend/src/app/_components/table/table.component.html +++ b/frontend/src/app/_components/table/table.component.html @@ -15,8 +15,7 @@ - - + red helmet red helmet {{ row.name }} diff --git a/frontend/src/app/_components/table/table.component.ts b/frontend/src/app/_components/table/table.component.ts index 9ac16bd..027fe04 100644 --- a/frontend/src/app/_components/table/table.component.ts +++ b/frontend/src/app/_components/table/table.component.ts @@ -74,10 +74,10 @@ export class TableComponent implements OnInit, OnDestroy { } onUserImpersonate(user: number) { - if(this.auth.profile.hasRole('SUPER_ADMIN')) { - this.auth.impersonate(user).then((user_id) => { + if(this.auth.profile.can('users-impersonate')) { + this.auth.impersonate(user).then(() => { this.loadTableData(); - this.userImpersonate.emit(user_id); + this.userImpersonate.emit(1); }); } } diff --git a/frontend/src/app/_guards/authorize.guard.ts b/frontend/src/app/_guards/authorize.guard.ts index 4a3a619..7ea10c6 100644 --- a/frontend/src/app/_guards/authorize.guard.ts +++ b/frontend/src/app/_guards/authorize.guard.ts @@ -15,7 +15,7 @@ export class AuthorizeGuard implements CanActivate { state: RouterStateSnapshot ): boolean { console.log(this.authService, route, state); - if(this.authService.profile === undefined) { + if(this.authService.profile.id === undefined) { console.log("not logged in"); this.router.navigate(['login', state.url.replace('/', '')]); return false; diff --git a/frontend/src/app/_routes/list/list.component.ts b/frontend/src/app/_routes/list/list.component.ts index 3699280..1d8228c 100644 --- a/frontend/src/app/_routes/list/list.component.ts +++ b/frontend/src/app/_routes/list/list.component.ts @@ -43,13 +43,13 @@ export class ListComponent implements OnInit, OnDestroy { changeAvailibility(available: 0|1, id?: number|undefined) { if(typeof id === 'undefined') { - id = this.auth.profile.auth_user_id; + id = this.auth.profile.id; } this.api.post("availability", { id: id, available: available }).then((response) => { - let changed_user_msg = parseInt(response.updated_user) === parseInt(this.auth.profile.auth_user_id) ? "La tua disponibilità" : `La disponibilità di ${response.updated_user_name}`; + let changed_user_msg = parseInt(response.updated_user_id) === parseInt(this.auth.profile.id) ? "La tua disponibilità" : `La disponibilità di ${response.updated_user_name}`; let msg = available === 1 ? `${changed_user_msg} è stata impostata con successo.` : `${changed_user_msg} è stata rimossa con successo.`; this.toastr.success(msg); this.loadAvailability(); diff --git a/frontend/src/app/_services/auth.service.ts b/frontend/src/app/_services/auth.service.ts index 09042a0..663100d 100644 --- a/frontend/src/app/_services/auth.service.ts +++ b/frontend/src/app/_services/auth.service.ts @@ -12,9 +12,12 @@ export interface LoginResponse { providedIn: 'root' }) export class AuthService { - public profile: any = { + private defaultPlaceholderProfile: any = { + id: undefined, + impersonating: false, can: (permission: string) => false }; + public profile: any = this.defaultPlaceholderProfile; public authChanged = new Subject(); public authLoaded = false; @@ -31,7 +34,7 @@ export class AuthService { resolve(); }).catch((e) => { console.error(e); - this.profile = undefined; + this.profile = this.defaultPlaceholderProfile; reject(); }).finally(() => { this.authChanged.next(); @@ -50,7 +53,7 @@ export class AuthService { } public isAuthenticated() { - return this.profile !== undefined; + return this.profile.id !== undefined; } public login(username: string, password: string) { @@ -94,31 +97,48 @@ export class AuthService { }) } - public impersonate(user_id: number): Promise { + public impersonate(user_id: number): Promise { return new Promise((resolve, reject) => { - resolve(0); + this.api.post(`impersonate/${user_id}`).then(() => { + this.loadProfile().then(() => { + resolve(); + }).catch((err) => { + console.error(err); + this.logout(); + this.profile.impersonating_user = false; + this.logout(); + }); + }).catch((err) => { + console.error(err); + reject(); + }); + }); + } + + public stop_impersonating(): Promise { + return new Promise((resolve, reject) => { + this.api.post("stop_impersonating").then(() => { + resolve(); + }).catch((err) => { + console.error(err); + reject(); + }); }); } public logout(routerDestination?: string[] | undefined) { - this.api.post("logout").then((data: any) => { - this.profile = undefined; - if(routerDestination === undefined) { - routerDestination = ["login", "list"]; - } - this.router.navigate(routerDestination); - }); - /* if(this.profile.impersonating_user) { - this.stop_impersonating().then((user_id) => { + this.stop_impersonating().then(() => { + this.loadProfile(); }); } else { - this.profile = undefined; - if(routerDestination === undefined) { - routerDestination = ["login", "list"]; - } - this.router.navigate(routerDestination); + this.api.post("logout").then((data: any) => { + this.profile = this.defaultPlaceholderProfile; + if(routerDestination === undefined) { + routerDestination = ["login", "list"]; + } + this.router.navigate(routerDestination); + }); } - */ } } diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 8e8dec6..6a2688e 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,9 +1,9 @@ -
+ diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 097a19f..1f0b802 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -5,6 +5,7 @@ "trainings": "Trainings", "logs": "Logs", "logout": "Logout", + "stop_impersonating": "Stop impersonating", "hi": "hi" }, "table": { diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index f492806..20db8a5 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -5,6 +5,7 @@ "trainings": "Esercitazioni", "logs": "Logs", "logout": "Logout", + "stop_impersonating": "Torna al vero account", "hi": "Ciao" }, "table": {