TenantAtlas/specs/083-required-permissions-hardening/plan.md

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/* without OperationRun
  • 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_permissions only.
  • 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)

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)

  1. Authorization + 404 semantics (page entry)
  • Update App\Filament\Pages\TenantRequiredPermissions to 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.
  1. Remove cross-plane tenant fallback
  • Make resolveScopedTenant() strict: only resolve from route {tenant} (bound model or external_id lookup). If absent/invalid → treat as not found.
  1. 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.
  1. 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/onboarding via route helper generation.
  1. Freshness / stale detection
  • Extend the view-model to include:
    • last_refreshed_at derived from stored tenant_permissions.last_checked_at (max)
    • is_stale (missing OR > 30 days)
  • Update overall status derivation to include stale as a warning.
  1. 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
  1. 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.
  1. Formatting + verification
  • Run vendor/bin/sail bin pint --dirty.
  • Run the targeted tests via vendor/bin/sail artisan test --compact ....