11 KiB
Implementation Plan: 083-required-permissions-hardening
Branch: 083-required-permissions-hardening | Date: 2026-02-08 | Spec: spec.md
Input: Feature specification from spec.md
Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.
Summary
Harden the canonical Required Permissions manage surface so it is only accessible via GET /admin/tenants/{tenant}/required-permissions, enforces deny-as-not-found (404) when the actor is not workspace-member or not tenant-entitled, removes any cross-plane tenant-context fallback, and presents issues-first UX using stored DB data only (no provider calls on render).
Research decisions are captured in research.md.
Technical Context
Language/Version: PHP 8.4.15 (Laravel 12)
Primary Dependencies: Filament v5 + Livewire v4, PostgreSQL, Tailwind CSS v4
Storage: PostgreSQL (Sail)
Testing: Pest v4 (run via Sail)
Target Platform: Web app (Laravel) running in Docker via Sail
Project Type: Web application (Laravel + Filament admin panel)
Performance Goals: Fast, DB-only page render (no outbound HTTP / Graph calls)
Constraints: Strict 404 vs 403 semantics (deny-as-not-found), no cross-plane tenant fallback
Scale/Scope: Single page hardening + view-model/UX changes + targeted tests
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
- Inventory-first: clarify what is “last observed” vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via
GraphClientInterface+config/graph_contracts.php - Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: manage surface (
/admin/tenants/...), tenant plane (/admin/t/{tenant}/...), and platform plane (/system/...) remain clearly separated; cross-plane access is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks) - RBAC-UX: destructive-like actions require
->requiresConfirmation()and clear warning text - RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses
OperationRun; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on/auth/*withoutOperationRun - Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use
BadgeCatalog/BadgeRenderer; no ad-hoc mappings; new values include tests - Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer
recordUrl()clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in “More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted
Gate evaluation (pre-design)
- Inventory-first / DB-only: PASS. This surface renders from stored
tenant_permissionsonly. - Read/write separation: PASS. The page is read-only; it only links to mutation surfaces.
- Graph contract path: PASS. No Graph calls on render; any verification runs remain elsewhere.
- Deterministic capabilities: PASS. Access is entitlement-based via tenant membership; capability checks remain on mutation surfaces.
- RBAC-UX semantics: PASS (planned). Implement explicit 404 denial for non-members/non-entitled and remove implicit tenant fallback.
- BADGE-001: PASS (planned). Use existing overall status enum values (
Blocked,NeedsAttention,Ready) and render via existing badge mechanisms. - Filament Action Surface Contract: PASS (exempt-by-design). This is a Filament Page (not a List/Table CRUD surface). It has no row/bulk actions; it is read-only and link-only.
Project Structure
Documentation (this feature)
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
Source Code (repository root)
app/
├── Filament/
│ ├── Pages/
│ │ └── TenantRequiredPermissions.php
│ └── Pages/Workspaces/
│ └── ManagedTenantOnboardingWizard.php # Start verification surface (CTA target)
├── Models/
│ ├── Tenant.php
│ ├── TenantPermission.php
│ ├── TenantMembership.php
│ ├── WorkspaceMembership.php
│ └── User.php
└── Services/
├── Auth/CapabilityResolver.php
└── Intune/
├── TenantPermissionService.php
└── TenantRequiredPermissionsViewModelBuilder.php
resources/
└── views/
└── filament/pages/tenant-required-permissions.blade.php
tests/
├── Feature/
│ ├── RequiredPermissions/ # to be created in Phase 2
│ │ ├── RequiredPermissionsAccessTest.php
│ │ ├── RequiredPermissionsDbOnlyRenderTest.php
│ │ ├── RequiredPermissionsEmptyStateTest.php
│ │ ├── RequiredPermissionsLegacyRouteTest.php
│ │ └── RequiredPermissionsLinksTest.php
└── Unit/
├── TenantRequiredPermissionsFreshnessTest.php
└── TenantRequiredPermissionsOverallStatusTest.php
Structure Decision: Web application (Laravel + Filament admin panel). Changes are localized to the Filament Page, its view-model builder, Blade view, and new targeted tests.
Complexity Tracking
Fill ONLY if Constitution Check has violations that must be justified
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
Phase 0 — Outline & Research (complete)
- Consolidated repo reality (existing canonical route, current tenant resolution fallback, current view-model behavior) and made explicit decisions in research.md.
- No remaining NEEDS CLARIFICATION items for Spec 083.
Phase 1 — Design & Contracts (complete)
- Data model notes captured in data-model.md.
- Route/semantics contract captured in contracts/routes.md.
- Developer quickstart captured in quickstart.md.
Constitution Check (post-design re-check)
- Tenant isolation / deny-as-not-found: PASS (design enforces explicit 404 for non-member/non-entitled).
- Cross-plane separation: PASS (design removes
Tenant::current()fallback on this surface). - Read/write separation: PASS (read-only page; mutation remains capability-gated on other surfaces).
- DB-only render: PASS (stored
tenant_permissions+ derived freshness). - Filament action contract: PASS (page is read-only; no list/table actions introduced).
Phase 1 — Agent context update (required)
Run:
.specify/scripts/bash/update-agent-context.sh copilot
Phase 2 — Implementation plan (input for tasks.md)
- Authorization + 404 semantics (page entry)
- Update
App\Filament\Pages\TenantRequiredPermissionsto enforce deny-as-not-found (404) when:- workspace not selected / tenant not found / tenant not in workspace
- actor not workspace member
- actor not tenant-entitled (
User::canAccessTenant($tenant)false)
- Ensure the checks run on initial page mount, not only in navigation gating.
- Remove cross-plane tenant fallback
- Make
resolveScopedTenant()strict: only resolve from route{tenant}(bound model orexternal_idlookup). If absent/invalid → treat as not found.
- DB-only render guarantees
- Confirm the view-model builder continues to call
TenantPermissionService::compare(... liveCheck:false ...). - Add tests to ensure no outbound HTTP is performed during render.
- Issues-first UX + canonical CTAs
- Update the Blade view to present:
- Summary (overall, counts, freshness)
- Issues (Blockers + Warnings only; no separate “Error” category)
- Passed / Technical details (de-emphasized, Technical collapsed by default)
- Add a dedicated empty-data state (“Keine Daten verfügbar”) with a links-only CTA to start verification.
- Update “Re-run verification” / “Start verification” link-only CTA to point canonical to
/admin/onboardingvia route helper generation.
- Freshness / stale detection
- Extend the view-model to include:
last_refreshed_atderived from storedtenant_permissions.last_checked_at(max)is_stale(missing OR > 30 days)
- Update overall status derivation to include stale as a warning.
- Tests (Pest) — minimum set
- Feature tests for:
- 404 for non-workspace-member
- 404 for workspace-member but not tenant-entitled
- 200 for tenant-entitled read-only
- empty-data state (“Keine Daten verfügbar”) with canonical start-verification CTA
- 404 for legacy route
/admin/t/{tenant}/required-permissions - 404 when route tenant missing/invalid (no fallback)
- Summary status mapping + stale threshold
- Technical details rendered after Issues/Passed and collapsed by default
- “Re-run verification” links to
/admin/onboarding
- Scope boundary for FR-083-009
- This feature does not modify mutation endpoints.
- Capability-based 403 enforcement remains on the linked target surfaces and is treated as an explicit dependency, not newly implemented behavior in Spec 083.
- Formatting + verification
- Run
vendor/bin/sail bin pint --dirty. - Run the targeted tests via
vendor/bin/sail artisan test --compact ....