Added moderator impersonation

This commit is contained in:
Julien Nahum 2024-01-19 14:27:04 +01:00
parent a651c60808
commit 42c65ae06f
12 changed files with 179 additions and 7 deletions

View File

@ -12,7 +12,7 @@ class ImpersonationController extends Controller
{ {
public function __construct() public function __construct()
{ {
$this->middleware('admin'); $this->middleware('moderator');
} }
public function impersonate($identifier) { public function impersonate($identifier) {
@ -29,12 +29,33 @@ class ImpersonationController extends Controller
} }
} }
if (!$user) return $this->error([ if (!$user) {
'message'=> 'User not found.' return $this->error([
'message'=> 'User not found.'
]);
} else if ($user->admin) {
return $this->error([
'message' => 'You cannot impersonate an admin.',
]);
}
\Log::warning('Impersonation started',[
'from_id' => auth()->id(),
'from_email' => auth()->user()->email,
'target_id' => $user->id,
'target_email' => $user->id,
]); ]);
// Be this user // Be this user
$token = auth()->login($user); if (auth()->user()->moderator) {
$token = auth()->claims([
'impersonating' => true,
'impersonator_id' => auth()->id(),
])->login($user);
} else {
$token = auth()->login($user);
}
return $this->success([ return $this->success([
'token' => $token 'token' => $token
]); ]);

View File

@ -5,7 +5,9 @@ namespace App\Http;
use App\Http\Middleware\AcceptsJsonMiddleware; use App\Http\Middleware\AcceptsJsonMiddleware;
use App\Http\Middleware\AuthenticateJWT; use App\Http\Middleware\AuthenticateJWT;
use App\Http\Middleware\CustomDomainRestriction; use App\Http\Middleware\CustomDomainRestriction;
use App\Http\Middleware\ImpersonationMiddleware;
use App\Http\Middleware\IsAdmin; use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsModerator;
use App\Http\Middleware\IsNotSubscribed; use App\Http\Middleware\IsNotSubscribed;
use App\Http\Middleware\IsSubscribed; use App\Http\Middleware\IsSubscribed;
use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Foundation\Http\Kernel as HttpKernel;
@ -58,6 +60,7 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\EncryptCookies::class, \App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
ImpersonationMiddleware::class,
], ],
]; ];
@ -72,6 +75,7 @@ class Kernel extends HttpKernel
'auth' => \App\Http\Middleware\Authenticate::class, 'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'admin' => IsAdmin::class, 'admin' => IsAdmin::class,
'moderator' => IsModerator::class,
'subscribed' => IsSubscribed::class, 'subscribed' => IsSubscribed::class,
'not-subscribed' => IsNotSubscribed::class, 'not-subscribed' => IsNotSubscribed::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,

View File

@ -0,0 +1,94 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class ImpersonationMiddleware
{
public const ADMIN_LOG_PREFIX = '[admin_action] ';
const LOG_ROUTES = [
'open.forms.store',
'open.forms.update',
'open.forms.duplicate',
'open.forms.regenerate-link',
];
const ALLOWED_ROUTES = [
'logout',
// Forms
'forms.ai.generate',
'forms.ai.show',
'forms.assets.show',
'forms.show',
'forms.answer',
'forms.fetchSubmission',
'forms.users.index',
'open.forms.index-all',
'open.forms.store',
'open.forms.assets.upload',
'open.forms.update',
'open.forms.duplicate',
'open.forms.regenerate-link',
'open.forms.submissions',
'open.forms.submissions.file',
// Workspaces
'open.workspaces.index',
'open.workspaces.create',
'open.workspaces.delete',
'open.workspaces.save-custom-domains',
'open.workspaces.databases.search',
'open.workspaces.databases.show',
'open.workspaces.form.stats',
'open.workspaces.forms.index',
'open.workspaces.users.index',
'templates.index',
'templates.create',
'templates.update',
'templates.show',
'user.current',
'local.temp',
];
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (!auth()->check() ||
!auth()->payload()->get('impersonating')) {
return $next($request);
}
// Check that route is allowed
$routeName = $request->route()->getName();
if (!in_array($routeName, self::ALLOWED_ROUTES)) {
return response([
'message' => 'Unauthorized when impersonating',
'route' => $routeName,
'impersonator' => auth()->payload()->get('impersonator_id'),
'impersonated_account' => auth()->id(),
'url' => $request->fullUrl(),
'payload' => $request->all()
], 403);
} else if (in_array($routeName, self::LOG_ROUTES)) {
\Log::warning(self::ADMIN_LOG_PREFIX . 'Impersonator action', [
'route' => $routeName,
'url' => $request->fullUrl(),
'impersonated_account' => auth()->id(),
'impersonator' => auth()->payload()->get('impersonator_id'),
'payload' => $request->all()
]);
}
return $next($request);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class IsModerator
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if ($request->user() && !$request->user()->moderator) {
// This user is not a paying customer...
if ($request->expectsJson()) {
return response([
'message' => 'You are not allowed.',
'type' => 'error',
], 403);
}
return redirect('home');
}
return $next($request);
}
}

View File

@ -18,6 +18,7 @@ class UserResource extends JsonResource
'is_subscribed' => $this->is_subscribed, 'is_subscribed' => $this->is_subscribed,
'has_enterprise_subscription' => $this->has_enterprise_subscription, 'has_enterprise_subscription' => $this->has_enterprise_subscription,
'admin' => $this->admin, 'admin' => $this->admin,
'moderator' => $this->moderator,
'template_editor' => $this->template_editor, 'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id, 'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms, 'has_forms' => $this->has_forms,

View File

@ -102,6 +102,11 @@ class User extends Authenticatable implements JWTSubject
return in_array($this->email, config('opnform.admin_emails')); return in_array($this->email, config('opnform.admin_emails'));
} }
public function getModeratorAttribute()
{
return in_array($this->email, config('opnform.moderator_emails')) || $this->admin;
}
public function getTemplateEditorAttribute() public function getTemplateEditorAttribute()
{ {
return $this->admin || in_array($this->email, config('opnform.template_editor_emails')); return $this->admin || in_array($this->email, config('opnform.template_editor_emails'));

View File

@ -100,6 +100,15 @@
Settings Settings
</NuxtLink> </NuxtLink>
<NuxtLink :to="{ name: 'settings-admin' }" v-if="user.moderator"
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
</svg>
Admin
</NuxtLink>
<a href="#" <a href="#"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="logout" @click.prevent="logout"

View File

@ -1,7 +1,6 @@
export default defineNuxtRouteMiddleware((to, from) => { export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore() const authStore = useAuthStore()
if (authStore.check && !authStore.user?.admin) { if (authStore.check && !authStore.user?.admin) {
console.log('redirecting to home')
return navigateTo({ name: 'home' }) return navigateTo({ name: 'home' })
} }
}) })

6
client/middleware/moderator.js vendored Normal file
View File

@ -0,0 +1,6 @@
export default defineNuxtRouteMiddleware((to, from) => {
const authStore = useAuthStore()
if (authStore.check && !authStore.user?.moderator) {
return navigateTo({ name: 'home' })
}
})

View File

@ -40,7 +40,7 @@ import {opnFetch} from "~/composables/useOpnApi.js";
import {fetchAllWorkspaces} from "~/stores/workspaces.js"; import {fetchAllWorkspaces} from "~/stores/workspaces.js";
definePageMeta({ definePageMeta({
middleware: "admin" middleware: "moderator"
}) })
useOpnSeoMeta({ useOpnSeoMeta({

View File

@ -2,6 +2,7 @@
return [ return [
'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? ''), 'admin_emails' => explode(",", env('ADMIN_EMAILS') ?? ''),
'moderator_emails' => explode(",", env('MODERATOR_EMAILS') ?? ''),
'template_editor_emails' => explode(",", env('TEMPLATE_EDITOR_EMAILS') ?? ''), 'template_editor_emails' => explode(",", env('TEMPLATE_EDITOR_EMAILS') ?? ''),
'extra_pro_users_emails' => explode(",", env('EXTRA_PRO_USERS_EMAILS') ?? ''), 'extra_pro_users_emails' => explode(",", env('EXTRA_PRO_USERS_EMAILS') ?? ''),
]; ];

View File

@ -37,7 +37,7 @@ use Illuminate\Support\Facades\Route;
Route::group(['middleware' => 'auth:api'], function () { Route::group(['middleware' => 'auth:api'], function () {
Route::post('logout', [LoginController::class, 'logout']); Route::post('logout', [LoginController::class, 'logout']);
Route::get('user', [UserController::class, 'current']); Route::get('user', [UserController::class, 'current'])->name('user.current');
Route::delete('user', [UserController::class, 'deleteAccount']); Route::delete('user', [UserController::class, 'deleteAccount']);
Route::patch('settings/profile', [ProfileController::class, 'update']); Route::patch('settings/profile', [ProfileController::class, 'update']);