TenantAtlas/app/Filament/Resources/TenantResource/Pages/ViewTenant.php
ahmido 3490fb9e2c feat: RBAC troubleshooting & tenant UI bugfix pack (spec 067) (#84)
Summary
Implements Spec 067 “RBAC Troubleshooting & Tenant UI Bugfix Pack v1” for the tenant admin plane (/admin) with strict RBAC UX semantics:

Non-member tenant scope ⇒ 404 (deny-as-not-found)
Member lacking capability ⇒ 403 server-side, while the UI stays visible-but-disabled with standardized tooltips
What changed
Tenant view header actions now use centralized UI enforcement (no “normal click → error page” for readonly members).
Archived tenants remain resolvable in tenant-scoped routes for entitled members; an “Archived” banner is shown.
Adds tenant-scoped diagnostics page (/admin/t/{tenant}/diagnostics) with safe repair actions (confirmation + authorization + audit log).
Adds/updates targeted Pest tests to lock the 404 vs 403 semantics and action UX.
Implementation notes
Livewire v4.0+ compliance: Uses Filament v5 + Livewire v4 conventions; widget Blade views render a single root element.
Provider registration: Laravel 11+ providers stay in providers.php (no changes required).
Global search: No global search behavior/resources changed in this PR.
Destructive actions:
Tenant archive/restore/force delete and diagnostics repairs execute via ->action(...) and include ->requiresConfirmation().
Server-side authorization is enforced (non-members 404, insufficient capability 403).
Assets: No new assets. No change to php artisan filament:assets expectations.
Tests
Ran:

vendor/bin/sail bin pint --dirty
vendor/bin/sail artisan test --compact (focused files for Spec 067)

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #84
2026-01-31 20:09:25 +00:00

109 lines
4.6 KiB
PHP

<?php
namespace App\Filament\Resources\TenantResource\Pages;
use App\Filament\Resources\TenantResource;
use App\Filament\Widgets\Tenant\TenantArchivedBanner;
use App\Models\Tenant;
use App\Services\Intune\AuditLogger;
use App\Services\Intune\RbacHealthService;
use App\Services\Intune\TenantConfigService;
use App\Services\Intune\TenantPermissionService;
use App\Support\Auth\Capabilities;
use App\Support\Rbac\UiEnforcement;
use Filament\Actions;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
class ViewTenant extends ViewRecord
{
protected static string $resource = TenantResource::class;
protected function getHeaderWidgets(): array
{
return [
TenantArchivedBanner::class,
];
}
protected function getHeaderActions(): array
{
return [
Actions\ActionGroup::make([
UiEnforcement::forAction(
Actions\Action::make('edit')
->label('Edit')
->icon('heroicon-o-pencil-square')
->url(fn (Tenant $record): string => TenantResource::getUrl('edit', ['record' => $record]))
)
->requireCapability(Capabilities::TENANT_MANAGE)
->apply(),
Actions\Action::make('admin_consent')
->label('Admin consent')
->icon('heroicon-o-clipboard-document')
->url(fn (Tenant $record) => TenantResource::adminConsentUrl($record))
->visible(fn (Tenant $record) => TenantResource::adminConsentUrl($record) !== null)
->openUrlInNewTab(),
Actions\Action::make('open_in_entra')
->label('Open in Entra')
->icon('heroicon-o-arrow-top-right-on-square')
->url(fn (Tenant $record) => TenantResource::entraUrl($record))
->visible(fn (Tenant $record) => TenantResource::entraUrl($record) !== null)
->openUrlInNewTab(),
Actions\Action::make('verify')
->label('Verify configuration')
->icon('heroicon-o-check-badge')
->color('primary')
->requiresConfirmation()
->action(function (
Tenant $record,
TenantConfigService $configService,
TenantPermissionService $permissionService,
RbacHealthService $rbacHealthService,
AuditLogger $auditLogger
) {
TenantResource::verifyTenant($record, $configService, $permissionService, $rbacHealthService, $auditLogger);
}),
TenantResource::rbacAction(),
UiEnforcement::forAction(
Actions\Action::make('archive')
->label('Deactivate')
->color('danger')
->icon('heroicon-o-archive-box-x-mark')
->visible(fn (Tenant $record): bool => ! $record->trashed())
->action(function (Tenant $record, AuditLogger $auditLogger): void {
$record->delete();
$auditLogger->log(
tenant: $record,
action: 'tenant.archived',
resourceType: 'tenant',
resourceId: (string) $record->getKey(),
status: 'success',
context: [
'metadata' => [
'internal_tenant_id' => (int) $record->getKey(),
'tenant_guid' => (string) $record->tenant_id,
],
]
);
Notification::make()
->title('Tenant deactivated')
->body('The tenant has been archived and hidden from lists.')
->success()
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::TENANT_DELETE)
->destructive()
->apply(),
])
->label('Actions')
->icon('heroicon-o-ellipsis-vertical')
->color('gray'),
];
}
}