Kontext / Ziel
Diese PR liefert den einzigen kanonischen Onboarding-Entry unter /admin/onboarding (workspace-first, tenantless bis zur Aktivierung) und ergänzt einen tenantless OperationRun-Viewer unter /admin/operations/{run} mit membership→404 Semantik.
Was ist enthalten?
Single entry point: /admin/onboarding ist der einzige Einstieg; Legacy Entry Points liefern echte 404 (keine Redirects).
Wizard v1 (Enterprise): idempotentes Identifizieren eines Managed Tenants (per Entra Tenant ID), resumable Session-Flow.
Provider Connection Step: Auswahl oder Erstellung, Secrets werden nie erneut gerendert / nicht in Session-State persistiert.
Verification als OperationRun: async/queued, DB-only Rendering im Wizard (keine Graph-Calls beim Rendern).
Tenantless Run Viewing: /admin/operations/{run} funktioniert ohne ausgewählten Workspace/Tenant, aber bleibt über Workspace-Mitgliedschaft autorisiert (non-member → 404).
RBAC-UX Semantik: non-member → 404, member ohne Capability → UI disabled + tooltip, server-side Action → 403.
Auditability: Aktivierung/Overrides sind auditierbar, stable action IDs, keine Secrets.
Tech / Version-Safety
Filament v5 / Livewire v4.0+ kompatibel.
Laravel 11+: Panel Provider Registrierung in providers.php (unverändert).
Tests / Format
vendor/bin/sail bin pint --dirty
Full suite: vendor/bin/sail artisan test --no-ansi → 984 passed, 5 skipped (exit 0)
Ops / Deployment Notes
Keine zusätzlichen Services vorausgesetzt.
Falls Assets registriert wurden: Deployment weiterhin mit php artisan filament:assets (wie üblich im Projekt).
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.fritz.box>
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #90
102 lines
3.3 KiB
PHP
102 lines
3.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
|
|
it('allows viewing an operation run without a selected workspace when the user is a member of the run workspace', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
session()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertSuccessful();
|
|
|
|
expect(session()->get(WorkspaceContext::SESSION_KEY))->toBeNull();
|
|
});
|
|
|
|
it('returns 404 for non-members when viewing an operation run without a selected workspace', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
session()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertNotFound();
|
|
});
|
|
|
|
it('renders stored target scope and failure details for a completed run', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$user = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $user->getKey(),
|
|
'role' => 'owner',
|
|
]);
|
|
|
|
session()->forget(WorkspaceContext::SESSION_KEY);
|
|
|
|
$entraTenantId = '11111111-1111-1111-1111-111111111111';
|
|
$failureMessage = 'Missing required Graph permissions.';
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'tenant_id' => null,
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Completed->value,
|
|
'outcome' => OperationRunOutcome::Failed->value,
|
|
'context' => [
|
|
'target_scope' => [
|
|
'entra_tenant_id' => $entraTenantId,
|
|
'entra_tenant_name' => 'Contoso',
|
|
],
|
|
],
|
|
'failure_summary' => [
|
|
[
|
|
'code' => 'provider.connection.check.failed',
|
|
'reason_code' => 'permission_denied',
|
|
'message' => $failureMessage,
|
|
],
|
|
],
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get("/admin/operations/{$run->getKey()}")
|
|
->assertSuccessful()
|
|
->assertSee($entraTenantId)
|
|
->assertSee('permission_denied')
|
|
->assertSee($failureMessage);
|
|
});
|