Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/Audit.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App;

use App\Enums\SessionVariables;
use Auth;
use Illuminate\Database\Eloquent\Model;

Expand All @@ -17,10 +18,21 @@ public function getTimeDateAttribute() {
}

public static function newAudit(string $message): void {
$impersonated_by_id = null;
$impersonation_string = '';
if (session()->has(SessionVariables::IMPERSONATING_USER->value)) {
$impersonated_by_id = session(SessionVariables::IMPERSONATING_USER->value);
$impersonation_user = User::find($impersonated_by_id);

$impersonation_string = 'IMPERSONATED BY ' . (is_null($impersonation_user) ? 'UNKNOWN' : $impersonation_user->full_name) . ': ';
}
$impersonated_by_id = session()->has(SessionVariables::IMPERSONATING_USER->value) ? session(SessionVariables::IMPERSONATING_USER->value) : null;

$audit = new Audit;
$audit->cid = Auth::id();
$audit->impersonated_by_id = $impersonated_by_id;
$audit->ip = $_SERVER['REMOTE_ADDR'];
$audit->what = Auth::user()->full_name . ' ' . $message;
$audit->what = $impersonation_string . Auth::user()->full_name . ' ' . $message;
$audit->save();
}
}
1 change: 1 addition & 0 deletions app/Enums/FeatureToggles.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ enum FeatureToggles: string {
case CUSTOM_THEME_LOGO = 'custom_theme_logo';
case LOCAL_HERO = 'local-hero';
case AUTO_SUPPORT_EVENTS = 'auto_support_events';
case IMPERSONATION = 'impersonation';
}
3 changes: 3 additions & 0 deletions app/Enums/SessionVariables.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

enum SessionVariables: string {
case SUCCESS = 'success';
case WARNING = 'warning';
case ERROR = 'error';
case VATSIM_AUTH_STATE = 'vatsimauthstate';
case REALOPS_PILOT_REDIRECT = 'pilot_redirect';
case REALOPS_PILOT_REDIRECT_PATH = 'pilot_redirect_path';
case IMPERSONATE = 'impersonate';
case IMPERSONATING_USER = 'impersonating_user';
}
34 changes: 34 additions & 0 deletions app/Http/Controllers/ImpersonationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Http\Controllers;

use App\Audit;
use App\Enums\SessionVariables;
use App\User;
use Illuminate\Http\Request;

class ImpersonationController extends Controller {
public function start(Request $request) {
$user = User::find($request->user_id);
$is_impersonating = session()->has(SessionVariables::IMPERSONATE->value);
if (is_null($user)) {
return redirect()->back()->with(SessionVariables::ERROR->value, 'That user does not exist');
}

if ($is_impersonating) {
return redirect()->back()->with(SessionVariables::ERROR->value, 'You must first stop impersonating your current user before beginning a new session');
}

session()->put(SessionVariables::IMPERSONATE->value, $user->id);
Audit::newAudit('started impersonating user ' . $user->impersonation_name . '.');

return redirect('/dashboard')->with(SessionVariables::ERROR->value, 'Successfully started impersonationg ' . $user->full_name . '. CAUTION: Impersonating actively logs you into the user\'s REAL account. Changes made while impersonating will be reflected on the user\'s actual account. PROCEED WITH CARE.');
}

public function stop() {
Audit::newAudit('impersonation session ending...');

session()->forget(SessionVariables::IMPERSONATE->value);
return redirect('/dashboard');
}
}
1 change: 1 addition & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Kernel extends HttpKernel {
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\Impersonation::class,
],

'api' => [
Expand Down
26 changes: 26 additions & 0 deletions app/Http/Middleware/Impersonation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Http\Middleware;

use App\Enums\FeatureToggles;
use App\Enums\SessionVariables;
use Auth;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class Impersonation {
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response {
if (toggleEnabled(FeatureToggles::IMPERSONATION) && session()->has(SessionVariables::IMPERSONATE->value) && Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm')) {
session()->put(SessionVariables::IMPERSONATING_USER->value, Auth::id());
Auth::onceUsingId(session(SessionVariables::IMPERSONATE->value));
}

return $next($request);
}
}
2 changes: 2 additions & 0 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Enums\FeatureToggles;
use App\Enums\SessionVariables;
use App\View\Composers\ImpersonationComposer;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
Expand All @@ -29,6 +30,7 @@ public function boot(): void {

View::share('FeatureToggles', FeatureToggles::class);
View::share('SessionVariables', SessionVariables::class);
View::composer('inc.dashboard_head', ImpersonationComposer::class);

/**
* Paginate a standard Laravel Collection.
Expand Down
16 changes: 16 additions & 0 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ public function getFullNameRatingAttribute() {
return $this->full_name . ' - ' . $this->rating_short;
}

public function getImpersonationNameAttribute() {
$roles = array_reduce($this->roles->toArray(), function ($role_string, $role) {
return $role_string . $role['name'] . ', ';
}, '');

if ($this->visitor) {
$roles = 'visitor';
}

if ($roles != '') {
$roles = ' (' . trim($roles, ', ') . ')';
}

return $this->backwards_name . ' ' . $this->id . ' - ' . $this->rating_short . $roles;
}

public static $RatingShort = [
0 => 'N/A',
1 => 'OBS', 2 => 'S1',
Expand Down
34 changes: 34 additions & 0 deletions app/View/Composers/ImpersonationComposer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\View\Composers;

use App\Enums\FeatureToggles;
use App\Enums\SessionVariables;
use App\User;
use Auth;
use Illuminate\View\View;

class ImpersonationComposer {
/**
* Create a new profile composer.
*/
public function __construct(
) {
}

/**
* Bind data to the view.
*/
public function compose(View $view): void {
if (toggleEnabled(FeatureToggles::IMPERSONATION)) {
$users = null;
$is_impersonating = session()->has(SessionVariables::IMPERSONATE->value);

if (Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm')) {
$users = User::where('status', 1)->orderBy('lname', 'ASC')->get()->pluck('impersonation_name', 'id');
}

$view->with('users', $users)->with('is_impersonating', $is_impersonating);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void {
Schema::table('audits', function (Blueprint $table) {
$table->integer('impersonated_by_id')->nullable();

$table->foreign('impersonated_by_id')->references('id')->on('roster')->nullOnDelete();
});
}

/**
* Reverse the migrations.
*/
public function down(): void {
Schema::table('audits', function (Blueprint $table) {
$table->dropColumn('impersonated_by_id');
});
}
};
46 changes: 25 additions & 21 deletions resources/views/inc/dashboard_head.blade.php
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
@include('inc.logo', ['color' => 'black'])
</a>
<div class="d-flex justify-content-start ms-5 collapse navbar-collapse">

{{ html()->form()->route('searchAirport')->class(['row','row-cols-lg-auto'])->open() }}
<div class="col-12 input-group">
{{ html()->text('apt', null)->placeholder('Search Airport ICAO')->class(['form-control']) }}
&nbsp;
<button class="btn btn-success" type="submit">Search</button>
</div>
<ul class="navbar-nav me-auto align-items-center">
{{ html()->form()->route('searchAirport')->class(['form-inline'])->open() }}
<div class="col-12 input-group">
{{ html()->text('apt', null)->placeholder('Search Airport ICAO')->class(['form-control']) }}
&nbsp;
<button class="btn btn-success" type="submit">Search</button>
</div>
{{ html()->form()->close() }}
</ul>
<ul class="navbar-nav ml-auto align-items-center">
<a class="nav-link {{ Nav::isRoute('controller_dash_home') }}" href="/dashboard">Dashboard Home</a>
@if(toggleEnabled($FeatureToggles::IMPERSONATION) && $is_impersonating)
<a class="nav-link" href="/dashboard/admin/impersonation/stop">End Impersonation</a>
@endif
@if(toggleEnabled($FeatureToggles::IMPERSONATION) && (Auth::user()->hasRole('wm') || Auth::user()->hasRole('awm')))
{{ html()->form()->route('startImpersonation')->class(['form-inline'])->open() }}
{{ html()->select('user_id', $users, Auth::id())->class(['form-select'])->attributes(['onchange' => 'this.form.submit()'])->disabled($is_impersonating) }}
{{ html()->form()->close() }}
</div>

<ul class="navbar-nav ms-auto">
<a class="nav-link {{ Nav::isRoute('controller_dash_home') }}" href="/dashboard">Dashboard Home</a>
<li class="nav-item dropdown">
<a class="nav-link" style="pointer-events:none">{{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }}</a>
</li>
</ul>
</div>
</div>
</nav>
@else
<a class="nav-link disabled">{{ Auth::user()->full_name }} - {{ Auth::user()->rating_short }}</a>
@endif
</ul>
</div>
</nav>
7 changes: 7 additions & 0 deletions resources/views/inc/messages.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
</div>
@endif

@if(session()->has($SessionVariables::WARNING->value))
<br>
<div class="alert alert-warning">
{{ session($SessionVariables::WARNING->value) }}
</div>
@endif

@if(session()->has($SessionVariables::ERROR->value))
<br>
<div class="alert alert-danger">
Expand Down
5 changes: 5 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,11 @@
Route::prefix('monitor')->middleware('permission:staff')->group(function () {
Route::get('/', 'AdminDash@backgroundMonitor');
});

Route::prefix('impersonation')->middleware('toggle:impersonation')->group(function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not guard the entire impersonation route? Is there a reason that start is guarded but stop is not? Trying to think if there is any way that the stop route could expose an attack vector...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stop can't be guarded because the middleware has to run on the impersonated user. If we don't allow stop to be hit by all users, we would never be able to end impersonation on non-privileged users.

Route::post('/', 'ImpersonationController@start')->middleware('role:wm|awm')->name('startImpersonation');
Route::get('/stop', 'ImpersonationController@stop')->name('stopImpersonation');
});
});
});
/*
Expand Down
Loading