Files

244 lines
11 KiB
PHP

<?php
namespace App\Providers;
use App\Auth\DockerSessionGuard;
use App\Models\Bank;
use App\Policies\BankPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
// Set remember me duration from platform configuration
$rememberMeDays = timebank_config('auth.remember_me_days', 90);
$rememberMeMinutes = $rememberMeDays * 24 * 60; // Convert days to minutes
// Use custom guard in Docker that doesn't migrate sessions
if (env('IS_DOCKER', false)) {
Auth::extend('session', function ($app, $name, array $config) use ($rememberMeMinutes) {
$provider = Auth::createUserProvider($config['provider']);
$guard = new DockerSessionGuard($name, $provider, $app['session.store']);
// Set the cookie jar on the guard
$guard->setCookieJar($app['cookie']);
// If a request is available, set it on the guard
if (method_exists($guard, 'setRequest')) {
$guard->setRequest($app->refresh('request', $guard, 'setRequest'));
}
// Set remember me duration
$guard->setRememberDuration($rememberMeMinutes);
return $guard;
});
}
// Set remember me duration for all standard guards
foreach (['web', 'organization', 'bank', 'admin'] as $guardName) {
$guard = Auth::guard($guardName);
if (method_exists($guard, 'setRememberDuration')) {
$guard->setRememberDuration($rememberMeMinutes);
}
}
// All permissions are strictly scoped to the active profile session.
//
// Rules:
// - Active profile = Admin{id}: only the scoped role Admin\{id}\{roleName}
// determines permissions. Roles from other linked admin profiles are ignored.
// - Active profile = Organization/Bank: only scoped Org/Bank roles apply.
// Admin-level permissions are NOT accessible when acting as Org/Bank.
// - Active profile = User or null/unknown: no admin permissions granted.
// Shared factory: returns a Gate closure that checks $permission on the active
// profile's single scoped role. Used for both manage and granular verbs.
$makeGate = function (string $permission) {
return function ($user) use ($permission) {
// Roles live on the User model (web guard). When the active guard is
// 'admin' (or another profile guard), Gate receives an Admin/Bank/Org
// model instead. Always resolve the web user explicitly.
if (!($user instanceof \App\Models\User)) {
$user = \Illuminate\Support\Facades\Auth::guard('web')->user();
if (!($user instanceof \App\Models\User)) {
return false;
}
}
try {
$activeType = session('activeProfileType');
$activeId = session('activeProfileId');
if (!$activeType || !$activeId) {
return false;
}
$prefixMap = [
'Admin' => "Admin\\{$activeId}\\",
'Organization' => "Organization\\{$activeId}\\",
'Bank' => "Bank\\{$activeId}\\",
];
$activeClass = class_basename($activeType);
$prefix = $prefixMap[$activeClass] ?? null;
if (!$prefix) {
return false; // User profile or unknown: no permissions
}
$scopedRoles = $user->roles
->filter(fn($r) => str_starts_with($r->name, $prefix));
if ($scopedRoles->isEmpty()) {
return false;
}
// Grant if any of the user's scoped roles for this profile has the permission.
// Also honour the manage→verb implication for granular checks.
$managePermission = null;
$parts = explode(' ', $permission, 2);
if (count($parts) === 2 && $parts[0] !== 'manage') {
$managePermission = 'manage ' . $parts[1];
}
foreach ($scopedRoles as $scopedRole) {
try {
if ($scopedRole->hasPermissionTo($permission, 'web')) {
return true;
}
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
// permission doesn't exist in DB, skip
}
if ($managePermission) {
try {
if ($scopedRole->hasPermissionTo($managePermission, 'web')) {
return true;
}
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
// manage permission doesn't exist, skip
}
}
}
return false;
} catch (\Spatie\Permission\Exceptions\PermissionDoesNotExist $e) {
return false;
} catch (\Exception $e) {
return false;
}
};
};
// manage {resource} — umbrella permission (checked by route middleware + mount())
foreach ([
'banks', 'organizations', 'admins', 'calls', 'posts', 'polls', 'tags',
'categories', 'mailings', 'profiles', 'roles', 'permissions',
'users', 'user_projects', 'accounts', 'maintenance',
] as $resource) {
Gate::define("manage {$resource}", $makeGate("manage {$resource}"));
}
// Granular action Gates — checked inside individual Livewire methods and blade views.
// Each resource lists only the verbs that make sense for it.
$granular = [
'posts' => ['view', 'create', 'update', 'publish', 'unpublish', 'delete'],
// Polls: 'export' covers who voted (Results tab) and the CSV export
'polls' => ['view', 'create', 'update', 'export', 'delete'],
'tags' => ['view', 'create', 'update', 'delete'],
'categories' => ['view', 'create', 'update', 'delete'],
'calls' => ['view', 'create', 'update', 'publish', 'unpublish', 'delete'],
'mailings' => ['view', 'create', 'update', 'send', 'delete'],
'users' => ['view', 'create', 'update', 'delete'],
'user_projects' => ['view', 'create', 'update', 'delete'],
'organizations' => ['view', 'create', 'update', 'delete'],
'banks' => ['view', 'create', 'update', 'delete'],
'admins' => ['view', 'create', 'update', 'delete'],
'accounts' => ['view', 'create', 'update', 'delete'],
'profiles' => ['view', 'create', 'update', 'delete'],
'permissions' => ['view', 'create', 'update', 'delete'],
'roles' => ['view', 'create', 'update', 'delete'],
'search-metrics' => ['view'],
'application-log' => ['view'],
'inactive-profiles-log' => ['view'],
'search-reindex-log' => ['view'],
'stats' => ['view'],
];
// Org/bank capability gates — checked inside org-context Livewire components.
foreach (['pay', 'transactions', 'reports', 'stats', 'contacts', 'calls', 'messenger', 'edit-profile', 'settings', 'change-password'] as $cap) {
Gate::define("org {$cap}", $makeGate("org {$cap}"));
}
// Admin capability gates — checked inside admin-context components.
foreach (['reports', 'contacts', 'messenger', 'settings', 'change-password'] as $cap) {
Gate::define("admin {$cap}", $makeGate("admin {$cap}"));
}
foreach ($granular as $resource => $verbs) {
foreach ($verbs as $verb) {
Gate::define("{$verb} {$resource}", $makeGate("{$verb} {$resource}"));
}
}
// Regular users can always manage their own calls — no role/permission needed.
// Ownership enforcement is done at the component level (query scoped to own calls).
$userCallVerbs = ['create', 'update', 'publish', 'unpublish', 'delete'];
foreach ($userCallVerbs as $verb) {
Gate::define("{$verb} calls", function ($user) use ($verb, $makeGate) {
$activeType = session('activeProfileType');
if ($activeType && class_basename($activeType) === 'User') {
return true;
}
return $makeGate("{$verb} calls")($user);
});
}
// access {resource} — page-level gate used on routes.
// 'view {resource}' grants read-only page entry; 'manage {resource}' grants full access.
// Roles without any permission for a resource cannot reach the page.
$accessResources = array_keys($granular);
foreach ($accessResources as $resource) {
Gate::define("access {$resource}", function ($user) use ($resource, $makeGate) {
return $makeGate("manage {$resource}")($user) || $makeGate("view {$resource}")($user);
});
}
// Route-level gate for the scoped bank-manager "manage accounts" surface
// (Profiles\Manage.php, non-central banks). Bank-scoped roles created via
// Roles\Manage.php only ever carry 'org *' style capability permissions —
// never the Admin-style 'manage accounts' permission the granular gates
// above check for — so this checks bank-manager role membership directly,
// the same convention getCanCreatePayments() already uses for banks.
Gate::define('bank manage accounts', function ($user) {
if (!($user instanceof \App\Models\User)) {
$user = \Illuminate\Support\Facades\Auth::guard('web')->user();
if (!($user instanceof \App\Models\User)) {
return false;
}
}
$activeType = session('activeProfileType');
$activeId = session('activeProfileId');
if ($activeType !== \App\Models\Bank::class || !$activeId) {
return false;
}
return $user->hasRole("Bank\\{$activeId}\\bank-manager");
});
}
}