Add support for impersonation

This commit is contained in:
Matteo Gheza 2023-06-06 18:53:49 +02:00
parent dec10cee4e
commit 10484739c9
17 changed files with 227 additions and 44 deletions

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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());
}
);
}
/**

View File

@ -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",

85
backend/composer.lock generated
View File

@ -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",

View File

@ -185,6 +185,7 @@ return [
/*
* Package Service Providers...
*/
Lab404\Impersonate\ImpersonateServiceProvider::class,
/*
* Application Service Providers...

View File

@ -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'
]
];

View File

@ -0,0 +1,41 @@
<?php
return [
/**
* The session key used to store the original user id.
*/
'session_key' => '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' => '/',
];

View File

@ -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']);

View File

@ -15,8 +15,7 @@
<tbody id="table_body">
<tr *ngFor="let row of data">
<td>
<!-- TODO: implement user impersonation -->
<i *ngIf="false && auth.profile.can('users-read') && row.id !== auth.profile.auth_user_id" class="fa fa-user me-2" (click)="onUserImpersonate(row.id)"></i>
<i *ngIf="auth.profile.can('users-impersonate') && row.id !== auth.profile.id" class="fa fa-user me-2" (click)="onUserImpersonate(row.id)"></i>
<img alt="red helmet" src="./assets/icons/red_helmet.png" width="20px" *ngIf="row.chief">
<img alt="red helmet" src="./assets/icons/black_helmet.png" width="20px" *ngIf="!row.chief">
<ng-container *ngIf="(getTime() - row.last_access) < 30"><u>{{ row.name }}</u></ng-container>

View File

@ -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);
});
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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<void>();
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<number> {
public impersonate(user_id: number): Promise<void> {
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<void> {
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);
});
}
*/
}
}

View File

@ -1,9 +1,9 @@
<div [className]="menuButtonClicked ? 'topnav responsive' : 'topnav'" id="topNavBar" *ngIf="auth.profile !== undefined">
<div [className]="menuButtonClicked ? 'topnav responsive' : 'topnav'" id="topNavBar" *ngIf="auth.profile.id !== undefined">
<a routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/list" translate>menu.list</a>
<a *ngIf="false" routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/services" translate>menu.services</a>
<a *ngIf="false" routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/trainings" translate>menu.trainings</a>
<a *ngIf="false" routerLinkActive="active" (click)="menuButtonClicked = false" routerLink="/logs" translate>menu.logs</a>
<a style="float: right;" id="logout">{{ 'menu.hi'|translate|titlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()" translate>menu.logout</b></a>
<a style="float: right;" id="logout">{{ 'menu.hi'|translate|titlecase }}, {{ auth.profile.name }}. <b id="logout-text" (click)="auth.logout()" translate *ngIf="!auth.profile.impersonating_user">menu.logout</b><b id="logout-text" (click)="auth.logout()" translate *ngIf="auth.profile.impersonating_user">menu.stop_impersonating</b></a>
<a class="icon" id="menuButton" (click)="menuButtonClicked = !menuButtonClicked"></a>
</div>

View File

@ -5,6 +5,7 @@
"trainings": "Trainings",
"logs": "Logs",
"logout": "Logout",
"stop_impersonating": "Stop impersonating",
"hi": "hi"
},
"table": {

View File

@ -5,6 +5,7 @@
"trainings": "Esercitazioni",
"logs": "Logs",
"logout": "Logout",
"stop_impersonating": "Torna al vero account",
"hi": "Ciao"
},
"table": {