Implements Spec 083 (Canonical Required Permissions manage surface hardening + issues-first UX).
Highlights:
- Enforces canonical route: /admin/tenants/{tenant}/required-permissions
- Legacy tenant-plane URL /admin/t/{tenant}/required-permissions stays non-existent (404)
- Deny-as-not-found (404) for non-workspace members and non-tenant-entitled users
- Strict tenant resolution (no cross-plane fallback)
- DB-only render (no external provider calls on page load)
- Issues-first layout + canonical next-step links (re-run verification -> /admin/onboarding)
- Freshness/stale detection (missing or >30 days -> warning)
Tests (Sail):
- vendor/bin/sail artisan test --compact tests/Feature/RequiredPermissions
- vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissionsFreshnessTest.php tests/Unit/TenantRequiredPermissionsOverallStatusTest.php
Notes:
- Filament v5 / Livewire v4 compliant.
- No destructive actions added in this spec; link-only CTAs.
Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #101
4.5 KiB
4.5 KiB
Research — Spec 083 (Required Permissions hardening)
Context recap
- Canonical manage-plane surface already exists as a Filament Page:
App\Filament\Pages\TenantRequiredPermissionswith slugtenants/{tenant}/required-permissions(admin panel). - A legacy tenant-plane path prefix exists (
/admin/t/...) via the tenant panel; spec requires/admin/t/{tenant}/required-permissionsto remain non-existent and return 404.
Decisions
Decision 1 — Canonical route stays /admin/tenants/{tenant}/required-permissions
- Chosen: Keep the canonical manage URL exactly as specified in Spec 083.
- Rationale: Already aligned with the existing page slug and with the established route contract in Spec 080.
- Alternatives considered:
- Redirect from
/admin/t/...→ rejected (spec requires 404, no redirect).
- Redirect from
Decision 2 — Deny-as-not-found is implemented explicitly (404), not via “canAccess() only”
- Chosen: Enforce 404 using explicit
abort(404)checks on request entry (e.g.,mount()), instead of relying solely on Filament’scanAccess()return value. - Rationale: Filament’s
canAccess()may produce behavior that is not guaranteed to be a 404. Spec 083 requires strict 404 semantics for non-members / non-entitled. - Alternatives considered:
- Only
canAccess()returning false → rejected (status code semantics uncertain). - Route-level middleware on just this page → possible, but still needs explicit entitlement checks; can be added later if desired.
- Only
Decision 3 — Tenant entitlement is checked via User::canAccessTenant($tenant)
- Chosen: Use
User::canAccessTenant()for tenant entitlement (no workspace-wide “view all tenants” override). - Rationale: This matches existing patterns across the codebase, uses
tenant_memberships, and aligns with the clarification outcome. - Alternatives considered:
- Workspace membership only → rejected (Spec 083 requires tenant entitlement).
- Capability checks for read-only view → rejected (read-only access is entitlement-only; mutations are capability-gated elsewhere).
Decision 4 — Remove cross-plane tenant fallback (Tenant::current()) from this surface
- Chosen:
TenantRequiredPermissions::resolveScopedTenant()must be strict: resolve only from route parameter{tenant}(external_id or bound model). If absent/invalid → 404. - Rationale:
Tenant::current()can leak cross-plane context and violates FR-083-007. - Alternatives considered:
- Keep fallback for convenience → rejected (security hardening goal).
Decision 5 — DB-only render is guaranteed by using stored tenant_permissions
- Chosen: Continue using
TenantPermissionService::compare(... liveCheck:false ...)for this page (no Graph calls). - Rationale: With
liveCheck=false, compare reads storedtenant_permissionsonly. - Alternatives considered:
- Allow live-check on render → rejected (violates FR-083-010).
Decision 6 — Freshness (“Last refreshed”) comes from tenant_permissions.last_checked_at
- Chosen: Define page “Last refreshed” as the max timestamp of stored permission checks for the tenant. Stale if missing or older than 30 days.
- Rationale: This is already stored in the database and does not require provider calls.
- Alternatives considered:
- Use latest verification run timestamps → possible, but increases coupling; not necessary for Spec 083.
Decision 7 — Summary status logic is centralized in the view-model builder
- Chosen: Update
TenantRequiredPermissionsViewModelBuilder::deriveOverallStatus()so:- Blocked if any missing application permission (blocker)
- Else Needs attention if any warning exists (missing delegated, error rows folded into warning, or stale freshness)
- Else Ready
- Rationale: Aligns with Spec 083 summary rules and keeps mapping centralized.
- Alternatives considered:
- Compute in Blade view → rejected (harder to test, risks drift).
Decision 8 — “Re-run verification” CTA links to /admin/onboarding (“Start verification” surface)
- Chosen: Link-only CTA points to the existing onboarding wizard page (admin panel slug
onboarding). - Rationale: Clarification outcome; capability gating occurs on the start/execute surface, not on this read-only page.
- Alternatives considered:
- Link to provider connection edit → rejected (not the requested primary action).
Open questions
None remaining for Spec 083 (clarifications already settled).