Spec 083: Required permissions hardening (canonical /admin/tenants, DB-only, 404 semantics) #101

Merged
ahmido merged 4 commits from 083-required-permissions-hardening into dev 2026-02-08 23:13:26 +00:00
8 changed files with 780 additions and 0 deletions
Showing only changes of commit 96760c65e6 - Show all commits

View File

@ -0,0 +1,39 @@
# Specification Quality Checklist: Canonical Required Permissions (Manage) Hardening & Enterprise UX
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-02-08
**Feature**: [specs/083-required-permissions-hardening/spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
Validation run (2026-02-08):
- Spec includes explicit 404 vs 403 semantics (deny-as-not-found for non-entitlement).
- Legacy URL non-existence is explicitly required and covered by test requirements.
- DB-only rendering constraint is explicitly required and test-covered.

View File

@ -0,0 +1,27 @@
# Route Contract — Spec 083
This contract defines the **Required Permissions** routes and their **404/403 semantics**.
## Canonical management surface (must exist)
- `GET /admin/tenants/{tenant}/required-permissions`
Identifier contract:
- `{tenant}` is `Tenant.external_id` (Entra tenant GUID)
Authorization contract:
- Not authenticated → handled by Filament auth middleware
- Workspace not selected → 404 (deny-as-not-found)
- Not a workspace member → 404
- Workspace member but **not tenant-entitled** (no `tenant_memberships` row) → 404
- Tenant-entitled (including read-only) → 200
Action contract:
- This page is read-only. Any mutations are only linked to and executed on other surfaces.
- Mutations on other surfaces must enforce capability checks server-side (missing capability → 403).
- "Re-run verification" links canonical to the start-verification surface: `GET /admin/onboarding` (generated via route helper, not hardcoded legacy paths).
## Removed tenant-plane route (must 404)
The following route MUST NOT exist and MUST return 404 (no redirects, no aliases):
- `GET /admin/t/{tenant}/required-permissions`

View File

@ -0,0 +1,40 @@
# Data Model — Spec 083
This feature is primarily **read-only UX + authorization hardening**. No new tables are required.
## Existing entities (relevant)
### Workspace
- **Purpose**: Isolation boundary for tenant management surfaces.
- **Key fields**: `id`.
### WorkspaceMembership
- **Purpose**: Establishes user membership in a workspace.
- **Key fields**: `workspace_id`, `user_id`, `role`.
### Tenant
- **Purpose**: Managed Entra tenant (scoped to a workspace).
- **Key fields**: `id`, `external_id` (Entra tenant GUID), `workspace_id`, `status`, `name`.
### TenantMembership
- **Purpose**: Tenant entitlement (read-only access at minimum).
- **Key fields**: `tenant_id`, `user_id`, `role`, `source`, `source_ref`.
### TenantPermission
- **Purpose**: Stored permission inventory used by Required Permissions page.
- **Key fields**: `tenant_id`, `permission_key`, `status` (`granted|missing|error`), `details` (JSON), `last_checked_at`.
## Derived / computed values
### "Last refreshed"
- **Definition**: `max(tenant_permissions.last_checked_at)` for the tenant.
- **Stale rule** (Spec 083): stale if missing OR older than 30 days.
### Summary overall status
Derived from stored permission rows (and freshness):
- **Blocked**: any missing `application` permission.
- **Needs attention**: any warning exists (missing delegated OR error rows folded into warning OR stale freshness).
- **Ready**: no blockers, no warnings.
## State transitions
- None introduced here (page remains read-only). Mutations happen on other surfaces (verification start, provider connection management) and must enforce capability checks there.

View File

@ -0,0 +1,204 @@
# Implementation Plan: 083-required-permissions-hardening
**Branch**: `083-required-permissions-hardening` | **Date**: 2026-02-08 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from [spec.md](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](research.md).
## Technical Context
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**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)
```text
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)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
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](research.md).
- No remaining NEEDS CLARIFICATION items for Spec 083.
## Phase 1 — Design & Contracts (complete)
- Data model notes captured in [data-model.md](data-model.md).
- Route/semantics contract captured in [contracts/routes.md](contracts/routes.md).
- Developer quickstart captured in [quickstart.md](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:
```bash
.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.
2. **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.
3. **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.
4. **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.
5. **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.
6. **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`
7. **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.
8. **Formatting + verification**
- Run `vendor/bin/sail bin pint --dirty`.
- Run the targeted tests via `vendor/bin/sail artisan test --compact ...`.

View File

@ -0,0 +1,31 @@
# Quickstart — Spec 083
## Dev prerequisites
- Run via Sail (local): Docker + `vendor/bin/sail` available.
## What to validate
### Route semantics
- Canonical (must exist): `GET /admin/tenants/{tenant}/required-permissions`
- Legacy (must 404): `GET /admin/t/{tenant}/required-permissions`
### Authorization semantics
- Non-workspace-member → 404
- Workspace-member but not tenant-entitled → 404
- Tenant-entitled (including read-only) → 200
### Render behavior
- Page render uses stored DB data only (no Graph / no outbound HTTP).
- If no stored permission data exists, page shows "Keine Daten verfügbar" with a canonical CTA to `/admin/onboarding`.
- "Technical details" appears after Issues/Passed and is collapsed by default.
## Run targeted tests
- `vendor/bin/sail artisan test --compact tests/Feature/RequiredPermissions/*`
- (Exact filenames to be created in Phase 2 tasks.)
## Manual smoke test
1. Log in to admin panel.
2. Select a workspace.
3. Open `/admin/tenants/{external_id}/required-permissions` for a tenant you are a member of.
4. Confirm Summary + Issues-first layout and that "Re-run verification" links to `/admin/onboarding`.
5. As a user without tenant entitlement, confirm the same URL returns 404.

View File

@ -0,0 +1,63 @@
# Research — Spec 083 (Required Permissions hardening)
## Context recap
- Canonical manage-plane surface already exists as a Filament Page: `App\Filament\Pages\TenantRequiredPermissions` with slug `tenants/{tenant}/required-permissions` (admin panel).
- A legacy tenant-plane path prefix exists (`/admin/t/...`) via the tenant panel; spec requires `/admin/t/{tenant}/required-permissions` to 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).
### 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 Filaments `canAccess()` return value.
- **Rationale**: Filaments `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.
### 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 stored `tenant_permissions` only.
- **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).

View File

@ -0,0 +1,214 @@
# Feature Specification: Canonical Required Permissions (Manage) Hardening & Enterprise UX
**Feature Branch**: `083-required-permissions-hardening`
**Created**: 2026-02-08
**Status**: Ready for implementation
**Input**: User description: "Harden the canonical Required Permissions manage surface: enforce tenant entitlement, keep legacy URL non-existent (404), remove cross-plane fallbacks, and improve issues-first UX without any provider calls."
## Clarifications
### Session 2026-02-08
- Q: Soll die optionale Workspace-weite Ausnahme „alle Tenants ansehen“ (ohne TenantMembership) Teil von Spec 083 sein? → A: Nein. Spec 083 basiert ausschließlich auf Tenant-Entitlement; kein „view all tenants“ Override.
- Q: Wie genau soll die Summary-Status-Logik (Blocked / Needs attention / Ready) definiert werden? → A: Blocked wenn mind. 1 Blocker; sonst Needs attention wenn mind. 1 Warning (inkl. stale); sonst Ready.
- Q: Ab wann gilt „Freshness“ als „stale“ (Warning)? → A: Warnung, wenn „Last refreshed“ fehlt oder älter als 30 Tage ist.
- Q: Soll die Seite einen expliziten „Error“-Issue-Typ anzeigen, oder nur Blocker/Warnings basierend auf gespeicherten Permission-Daten? → A: Kein „Error“-Issue-Typ in Spec 083. Nur Blocker (missing application) + Warnings (delegated/stale/unknown).
- Q: Wohin soll der links-only CTA „Re-run verification“ canonical führen? → A: Zur „Start verification“ Surface (Wizard/Startseite), damit ein neuer Run gestartet werden kann (capability-gated dort).
## User Scenarios & Testing *(mandatory)*
<!--
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
you should still have a viable MVP (Minimum Viable Product) that delivers value.
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
Think of each story as a standalone slice of functionality that can be:
- Developed independently
- Tested independently
- Deployed independently
- Demonstrated to users independently
-->
### User Story 1 - Required Permissions sicher ansehen (Priority: P1)
Als Workspace-Mitglied mit Tenant-Entitlement möchte ich die "Required Permissions" Seite eines Tenants öffnen, um sofort zu erkennen, ob administrative Berechtigungen fehlen (Blocker) oder ob nur Hinweise/Warnings bestehen — ohne dass dadurch externe Provider-Aufrufe ausgelöst werden.
**Why this priority**: Das ist die primäre, risikorelevante Enterprise-UX: Security- und Operations-Teams müssen schnell und sicher einschätzen können, ob Handlungsbedarf besteht.
**Independent Test**: Kann vollständig über einen einzelnen GET-Aufruf auf die Canonical-URL getestet werden, inklusive 200/404 Semantik, UI-Sektionen und „keine externen Calls“.
**Acceptance Scenarios**:
1. **Given** ein User ist Workspace-Mitglied und tenant-entitled, **When** er die Canonical-URL für den Tenant öffnet, **Then** erhält er 200 und sieht eine issues-first Zusammenfassung (Summary → Issues → Passed → Technical).
2. **Given** die Seite wird aufgerufen, **When** sie gerendert wird, **Then** werden keine externen Provider-Anfragen ausgelöst (nur gespeicherte Daten werden verwendet).
---
### User Story 2 - Next steps finden, ohne Mutationsrechte zu benötigen (Priority: P2)
Als tenant-entitled User möchte ich auf der Seite klare "Next steps" sehen (links-only), um fehlende Berechtigungen zu beheben oder eine erneute Verifikation anzustoßen, ohne dass ich selbst zwingend Mutationsrechte habe.
**Why this priority**: In Enterprise-Umgebungen sind Rollen getrennt: Viewer müssen Probleme erkennen und korrekt eskalieren können, ohne selbst Änderungen durchführen zu dürfen.
**Independent Test**: Kann über Render-Assertions getestet werden: Issue-Karten enthalten ausschließlich Links zu passenden Folgeseiten, und die Links sind canonical.
**Acceptance Scenarios**:
1. **Given** es existieren Blocker/Warnings, **When** die Seite gerendert wird, **Then** enthält jede Issue eine klare, links-only Handlungsempfehlung (z.B. „Admin consent dokumentieren“, „Verifikation erneut starten“, „Provider-Verbindung verwalten“).
2. **Given** Next-step Links werden angezeigt, **When** die URLs geprüft werden, **Then** verweisen sie auf die canonical Manage-Surfaces und nicht auf Legacy-Tenant-Plane URLs.
---
### User Story 3 - Tenant-Discovery verhindern (Deny-as-not-found) (Priority: P3)
Als Security Owner möchte ich, dass Workspace-Mitglieder ohne Tenant-Entitlement weder über URL-Varianten noch über Fehlermeldungen Hinweise auf die Existenz eines Tenants oder dessen Security-Posture erhalten.
**Why this priority**: Verhindert Tenant-Leakage und erzwingt eine konsistente Enterprise-Sicherheitsposition.
**Independent Test**: Kann isoliert über negative Access-Tests (404 Semantik) für verschiedene Benutzerzustände getestet werden.
**Acceptance Scenarios**:
1. **Given** ein User ist Workspace-Mitglied ohne Tenant-Entitlement, **When** er die canonical Required-Permissions URL eines Tenants aufruft, **Then** erhält er 404 (deny-as-not-found).
2. **Given** ein User ruft eine Legacy-Tenant-Plane URL-Variante auf, **When** der Request verarbeitet wird, **Then** ist das Ergebnis 404 (keine Redirects, keine Aliases).
---
### Edge Cases
- Tenant-ID ist syntaktisch ungültig oder verweist auf keinen Tenant → 404.
- Tenant gehört nicht zum aktuell selektierten Workspace → 404.
- Workspace ist nicht selektiert / User ist kein Workspace-Mitglied → 404.
- Es existieren keine gespeicherten Daten (noch nie verifiziert / gelöscht) → Seite erklärt „keine Daten verfügbar“ und verlinkt zur Verifikation.
- Daten sind alt (stale) → Warning + Link zu „erneut verifizieren“.
- Freshness ist unbekannt (kein „Last refreshed“) → Warning + Link zu „erneut verifizieren“.
## Requirements *(mandatory)*
**Constitution alignment (required):** If this feature introduces any Microsoft Graph calls, any write/change behavior,
or any long-running/queued/scheduled work, the spec MUST describe contract registry updates, safety gates
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
- state which authorization plane(s) are involved (tenant `/admin/t/{tenant}` vs platform `/system`),
- ensure any cross-plane access is deny-as-not-found (404),
- explicitly define 404 vs 403 semantics:
- non-member / not entitled to tenant scope → 404 (deny-as-not-found)
- member but missing capability → 403
- describe how authorization is enforced server-side (Gates/Policies) for every mutation/operation-start/credential change,
- reference the canonical capability registry (no raw capability strings; no role-string checks in feature code),
- ensure global search is tenant-scoped and non-member-safe (no hints; inaccessible results treated as 404 semantics),
- ensure destructive-like actions require confirmation (`->requiresConfirmation()`),
- include at least one positive and one negative authorization test, and note any RBAC regression tests added/updated.
**Constitution alignment (OPS-EX-AUTH-001):** OIDC/SAML login handshakes may perform synchronous outbound HTTP (e.g., token exchange)
on `/auth/*` endpoints without an `OperationRun`. This MUST NOT be used for Monitoring/Operations pages.
**Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean),
the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values.
**Constitution alignment (Filament Action Surfaces):** If this feature adds or modifies any Filament Resource / RelationManager / Page,
the spec MUST include a “UI Action Matrix” (see below) and explicitly state whether the Action Surface Contract is satisfied.
If the contract is not satisfied, the spec MUST include an explicit exemption with rationale.
<!--
ACTION REQUIRED: The content in this section represents placeholders.
Fill them out with the right functional requirements.
-->
### Functional Requirements
#### Surfaces & Routing
- **FR-083-001**: Die Required-Permissions Oberfläche MUSS ausschließlich auf der canonical Manage-URL verfügbar sein: `GET /admin/tenants/{tenant}/required-permissions`.
- **FR-083-002**: Eine Legacy-Tenant-Plane Variante MUSS nicht existieren und MUSS 404 liefern: `GET /admin/t/{tenant}/required-permissions` (keine Redirects, keine Aliases).
#### Authorization (Enterprise Hardening)
- **FR-083-003**: Die Seite MUSS deny-as-not-found (404) verwenden, wenn der User kein Workspace-Mitglied ist.
- **FR-083-004**: Die Seite MUSS deny-as-not-found (404) verwenden, wenn der User Workspace-Mitglied ist, aber kein Tenant-Entitlement besitzt.
- **FR-083-005**: Die Seite MUSS 200 liefern, wenn der User Workspace-Mitglied ist und Tenant-Entitlement besitzt (inkl. Readonly-Entitlement).
- **FR-083-006**: Der Route-Parameter `{tenant}` MUSS vorhanden sein und einem Tenant im aktuell selektierten Workspace entsprechen; fehlt der Parameter oder ist er ungültig, MUSS 404 zurückgegeben werden.
- **FR-083-007**: Die Seite MUSS strikt an den URL-Tenant gebunden sein; es darf keinen impliziten Fallback auf einen „aktuellen“ Tenant-Kontext geben.
#### 404 vs 403 Semantik (RBAC-UX)
- **FR-083-008**: 404-Antworten bei Membership-/Entitlement-Denial MÜSSEN generisch bleiben und dürfen keinen Ablehnungsgrund offenlegen (kein Tenant-Leakage).
- **FR-083-009**: Falls auf der Seite Aktionen/Mutations verlinkt werden (z.B. „Verifikation starten“), MUSS die eigentliche Mutation server-seitig capability-gated sein und bei fehlender Fähigkeit 403 liefern. Die Required-Permissions Seite selbst bleibt read-only; die 403-Durchsetzung wird auf den Ziel-Surfaces umgesetzt (kein zusätzlicher Mutations-Endpunkt in Spec 083).
#### Data Source & External Calls
- **FR-083-010**: Das Anzeigen der Seite MUSS ausschließlich gespeicherte Daten verwenden und darf keine externen Provider-Aufrufe auslösen.
#### UX (Issues-first)
- **FR-083-011**: Die Seite MUSS oben eine Summary zeigen, die die Gesamtlage verständlich einordnet (z.B. „Blocked / Needs attention / Ready“) und die wichtigsten Counts enthält.
- **FR-083-011a**: Die Summary-Status-Logik MUSS eindeutig sein: **Blocked** wenn mindestens ein Blocker vorliegt; sonst **Needs attention** wenn mindestens ein Warning vorliegt (inkl. „stale“); sonst **Ready**.
- **FR-083-012**: Die Seite MUSS prominent eine Issues-Sektion bereitstellen, die Blocker (fehlende Application-Berechtigungen) und Warnings (z.B. delegated gaps, stale data) priorisiert.
- **FR-083-012a**: Die Issues-Sektion MUSS sich in Spec 083 auf **Blocker** und **Warnings** beschränken; ein separater „Error“-Issue-Typ ist nicht Teil des Umfangs.
- **FR-083-013**: Jede Issue MUSS links-only Next steps enthalten (keine eingebetteten Mutations) und klar zwischen „Beheben“ und „erneut verifizieren“ unterscheiden.
- **FR-083-013a**: Der links-only CTA „Re-run verification“ MUSS canonical zur „Start verification“ Surface `/admin/onboarding` führen und über zentrale Route-Generierung erstellt werden (kein hardcodierter Legacy-Pfad). Die capability-basierte Durchsetzung (403) erfolgt dort, nicht auf der Required-Permissions Seite.
- **FR-083-014**: Die Seite MUSS einen Hinweis enthalten, dass die Anzeige auf gespeicherten Daten basiert, inkl. Freshness/Last refreshed Information, sofern aus gespeicherten Daten ableitbar.
- **FR-083-014a**: Freshness MUSS als Warning gelten, wenn „Last refreshed“ fehlt oder älter als 30 Tage ist.
- **FR-083-014b**: Wenn keine gespeicherten Permission-Daten vorhanden sind, MUSS die Seite einen klaren Empty State („Keine Daten verfügbar“) rendern und einen links-only CTA zur Start-verification Surface anzeigen.
- **FR-083-015**: „Technical details“ MUSS verfügbar sein, aber nachrangig: die Sektion MUSS nach „Issues“ und „Passed“ erscheinen und standardmäßig eingeklappt sein.
#### Link Consistency
- **FR-083-016**: In-App Links zur Required-Permissions Oberfläche MÜSSEN canonical sein und konsistent generiert werden (keine hardcodierten Legacy-Pfade).
#### Dependencies & Assumptions
- **FR-083-017**: Die Seite baut auf existierenden Manage-Surfaces für Tenants, Verifikation und Provider-Verbindungen auf (nur Verlinkung; keine neue Surface wird dadurch eingeführt).
- **FR-083-018**: Es existiert ein Konzept von Workspace-Mitgliedschaft und Tenant-Entitlement; Entitlement ist die Voraussetzung für read-only Zugriff.
#### Test Requirements (Mandatory)
- **T-083-001**: Kein Workspace-Mitglied → 404.
- **T-083-002**: Workspace-Mitglied ohne Tenant-Entitlement → 404.
- **T-083-003**: Tenant-entitled User (Readonly) → 200.
- **T-083-004**: Keine gespeicherten Daten → Seite zeigt „Keine Daten verfügbar“ und einen canonical CTA zur Start-verification Surface.
- **T-083-005**: DB-only Render: canonical URL rendert ohne externe Provider-Requests und ohne Hintergrundarbeit auszulösen.
- **T-083-006**: Legacy URL bleibt 404: `/admin/t/{tenant}/required-permissions`.
- **T-083-007**: Link canonicalization: Next steps enthalten ausschließlich canonical Manage-Links.
- **T-083-008**: Cross-plane fallback Regression: Aufruf ohne gültigen Route-Tenant darf keinen impliziten „aktuellen Tenant“ nutzen → 404.
- **T-083-009**: Summary-Status-Logik: Blocker → „Blocked“; nur Warnings/Stale → „Needs attention“; keine Issues → „Ready“.
- **T-083-010**: Stale-Threshold: „Last refreshed“ fehlt oder älter als 30 Tage → Warning; jünger/gleich 30 Tage → kein Freshness-Warning.
- **T-083-011**: Issues-Typen: Seite zeigt keine separate „Error“-Issue-Kategorie (nur Blocker + Warnings).
- **T-083-012**: „Re-run verification“ Link führt canonical zur „Start verification“ Surface (kein Link auf „latest report“ als Primärziel).
- **T-083-013**: „Technical details“ ist standardmäßig eingeklappt und erscheint nach „Issues“ und „Passed“.
## UI Action Matrix *(mandatory when Filament is changed)*
Für jede betroffene UI-Oberfläche: liste die sichtbaren Actions/CTAs, ob sie destruktiv sind (Bestätigung erforderlich),
welche Autorisierung gilt (Entitlement vs. Fähigkeit für Mutationen), und ob ein Audit-Eintrag erwartet wird.
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|
| Admin Page: Required Permissions | Admin → Tenants → Required permissions | None (read-only) | N/A | None | None | Links-only: “Start verification”, “Manage provider connection” | N/A | N/A | No (view-only) | Verlinkte Mutations/Aktionen liegen auf anderen Surfaces und müssen dort 403/capability-gated sein |
### Key Entities *(include if feature involves data)*
- **Workspace**: Sicherheits- und Sichtbarkeitsgrenze; ein User muss Mitglied sein, um Tenant-Surfaces überhaupt sehen zu können.
- **Tenant**: Mandant im Workspace; Required Permissions sind tenant-spezifisch.
- **Workspace Membership**: Belegt, dass ein User zum Workspace gehört.
- **Tenant Entitlement (Tenant Membership)**: Belegt, dass ein User in diesem Tenant lesen darf (inkl. Readonly).
- **Permission Inventory Snapshot**: Gespeicherte Datenbasis, aus der Required-Permissions Status/Issues abgeleitet werden.
- **Verification Evidence / Report**: Gespeicherte Ergebnisse, die Freshness/Last refreshed und Issues erklären und auf „erneut verifizieren“ verlinken.
## Success Criteria *(mandatory)*
<!--
ACTION REQUIRED: Define measurable success criteria.
These must be technology-agnostic and measurable.
-->
### Measurable Outcomes
- **SC-083-001**: 100% der Requests von nicht-entitled Workspace-Mitgliedern auf die Required-Permissions Seite enden in 404 (kein Tenant-Leakage über Statuscodes).
- **SC-083-002**: 100% der Requests auf die Legacy-URL-Variante `/admin/t/{tenant}/required-permissions` enden in 404 (keine Redirects).
- **SC-083-003**: Beim Anzeigen der Seite werden 0 externe Provider-Anfragen ausgelöst (verifizierbar über Tests/Instrumentation).
- **SC-083-004**: Tenant-entitled Nutzer können in ≤ 30 Sekunden mindestens einen Blocker identifizieren und den passenden Next-step Link finden (Usability/UX-Verifikation).
- **SC-083-005**: In Staging liegt für `GET /admin/tenants/{tenant}/required-permissions` bei einer typischen Tenant-Datenmenge (mindestens 200 gespeicherte Permission-Zeilen) die p95-Server-Antwortzeit bei ≤ 500 ms (DB-only, ohne externe Provider-Calls).

View File

@ -0,0 +1,162 @@
---
description: "Task list for Spec 083-required-permissions-hardening"
---
# Tasks: 083-required-permissions-hardening
**Input**: Design documents from `/specs/083-required-permissions-hardening/`
- Spec: [spec.md](spec.md)
- Plan: [plan.md](plan.md)
- Research: [research.md](research.md)
- Data model: [data-model.md](data-model.md)
- Contracts: [contracts/routes.md](contracts/routes.md)
- Quickstart: [quickstart.md](quickstart.md)
**Tests**: REQUIRED (Pest) — runtime behavior changes.
## Phase 1: Setup (Shared Infrastructure)
- [X] T001 Run prerequisites check via .specify/scripts/bash/check-prerequisites.sh --json
- [X] T002 Ensure agent context is up to date via .specify/scripts/bash/update-agent-context.sh copilot
- [X] T003 [P] Create feature test directory tests/Feature/RequiredPermissions/ (add .gitkeep if needed)
---
## Phase 2: Foundational (Blocking Prerequisites)
- [X] T004 Review current canonical page implementation in app/Filament/Pages/TenantRequiredPermissions.php (identify tenant fallback + current access checks)
- [X] T005 [P] Review existing DB-only render guard patterns in tests/Feature/Auth/DbOnlyPagesDoNotMakeHttpRequestsTest.php (copy the Http::preventStrayRequests() approach)
- [X] T006 [P] Review existing cross-plane 404 patterns in tests/Feature/Auth/CrossScopeAccessTest.php (align with 404 semantics)
- [X] T007 [P] Confirm factories exist for required models (Workspace, WorkspaceMembership, Tenant, TenantMembership, TenantPermission, User) under database/factories/
**Checkpoint**: Foundational ready — implement US1/US2/US3.
---
## Phase 3: User Story 1 — Required Permissions sicher ansehen (Priority: P1) 🎯 MVP
**Goal**: Canonical manage surface renders issues-first from DB-only state with correct 200/404 semantics.
**Independent Test**: A single GET to `/admin/tenants/{external_id}/required-permissions` returns 200 for tenant-entitled users and triggers no outbound HTTP.
### Tests (US1)
- [X] T008 [P] [US1] Add DB-only render test in tests/Feature/RequiredPermissions/RequiredPermissionsDbOnlyRenderTest.php
- [X] T009 [P] [US1] Add happy-path entitlement test (tenant-entitled → 200) in tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php
- [X] T030 [P] [US1] Add empty-data state test ("Keine Daten verfügbar" + Start verification CTA) in tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php
- [X] T031 [P] [US1] Add test that "Technical details" is rendered after Issues/Passed and is collapsed by default in tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php
### Implementation (US1)
- [X] T010 [US1] Enforce explicit 404 denial rules on page entry in app/Filament/Pages/TenantRequiredPermissions.php (workspace selected, tenant in workspace, workspace member, tenant-entitled)
- [X] T011 [US1] Remove cross-plane fallback by making resolveScopedTenant() strict (no Tenant::current()) in app/Filament/Pages/TenantRequiredPermissions.php
- [X] T012 [US1] Add freshness derivation (last_refreshed_at, is_stale) based on tenant_permissions.last_checked_at in app/Services/Intune/TenantRequiredPermissionsViewModelBuilder.php
- [X] T013 [US1] Update summary overall status derivation to treat stale freshness as a warning (Blocked > Needs attention > Ready) in app/Services/Intune/TenantRequiredPermissionsViewModelBuilder.php
- [X] T014 [US1] Render Summary → Issues → Passed → Technical layout (issues-first) using viewModel fields in resources/views/filament/pages/tenant-required-permissions.blade.php
- [X] T032 [US1] Render explicit empty-data state and keep "Technical details" collapsed by default in resources/views/filament/pages/tenant-required-permissions.blade.php
---
## Phase 4: User Story 2 — Next steps finden, ohne Mutationsrechte zu benötigen (Priority: P2)
**Goal**: Each issue includes link-only next steps that point to canonical manage surfaces; re-run verification links to Start verification.
**Independent Test**: Page renders next-step links that are canonical and the “Re-run verification” CTA points to `/admin/onboarding`.
### Tests (US2)
- [X] T015 [P] [US2] Add CTA/link assertion test for re-run verification pointing to /admin/onboarding in tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php
- [X] T016 [P] [US2] Add test asserting no legacy tenant-plane links are emitted (no /admin/t/...) in tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php
### Implementation (US2)
- [X] T017 [US2] Change reRunVerificationUrl() to return the canonical Start verification surface via route helper (target: /admin/onboarding) in app/Filament/Pages/TenantRequiredPermissions.php
- [X] T018 [US2] Ensure issue cards only contain link-only next steps and canonical manage URLs in resources/views/filament/pages/tenant-required-permissions.blade.php
---
## Phase 5: User Story 3 — Tenant-Discovery verhindern (Deny-as-not-found) (Priority: P3)
**Goal**: Non-entitled users cannot discover tenant existence/posture via status codes or legacy routes.
**Independent Test**: Requests for non-members/non-entitled return 404, and legacy `/admin/t/{tenant}/required-permissions` is 404.
### Tests (US3)
- [X] T019 [P] [US3] Add test: workspace-member but not tenant-entitled → 404 in tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php
- [X] T020 [P] [US3] Add test: not a workspace member → 404 in tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php
- [X] T021 [P] [US3] Add test: legacy /admin/t/{tenant}/required-permissions returns 404 in tests/Feature/RequiredPermissions/RequiredPermissionsLegacyRouteTest.php
- [X] T022 [P] [US3] Add regression test: route tenant invalid does not fall back to a current tenant context (still 404) in tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php
### Implementation (US3)
- [X] T023 [US3] Ensure all deny-as-not-found conditions abort(404) (not 403) in app/Filament/Pages/TenantRequiredPermissions.php
---
## Phase 6: Polish & Cross-Cutting Concerns
- [X] T024 [P] Update existing unit coverage for overall status if signature/logic changed in tests/Unit/TenantRequiredPermissionsOverallStatusTest.php
- [X] T025 [P] Add new unit tests for freshness/stale threshold (missing or >30 days) in tests/Unit/TenantRequiredPermissionsFreshnessTest.php
- [X] T026 Run formatting via vendor/bin/sail bin pint --dirty
- [X] T027 Run targeted tests via vendor/bin/sail artisan test --compact tests/Feature/RequiredPermissions
- [X] T028 Run targeted unit tests via vendor/bin/sail artisan test --compact tests/Unit/TenantRequiredPermissions
- [X] T029 Validate quickstart steps remain accurate in specs/083-required-permissions-hardening/quickstart.md
---
## Dependencies & Execution Order
### User Story completion order
```mermaid
graph TD
P1[US1: View canonical page safely] --> P2[US2: Canonical next steps links]
P1 --> P3[US3: Deny-as-not-found + legacy 404]
P2 --> Polish[Polish & regression coverage]
P3 --> Polish
```
- Setup (T001T003) → Foundational (T004T007) → US1 (T008T014, T030T032) → US2 (T015T018) + US3 (T019T023) → Polish (T024T029)
### Parallel opportunities
- Phase 1: T003 can run in parallel.
- Phase 2: T005T007 are parallel.
- US1 tests (T008T009, T030T031) can be written in parallel.
- US2 tests (T015T016) can be written in parallel.
- US3 tests (T019T022) can be written in parallel.
- Polish: T024T025 are parallel; T026T028 are sequential validation.
---
## Parallel execution examples (per story)
### US1
- Run in parallel:
- T008: tests/Feature/RequiredPermissions/RequiredPermissionsDbOnlyRenderTest.php
- T009: tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php
- T030: tests/Feature/RequiredPermissions/RequiredPermissionsEmptyStateTest.php
- T031: tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php
### US2
- Run in parallel:
- T015: tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php (CTA)
- T016: tests/Feature/RequiredPermissions/RequiredPermissionsLinksTest.php (no legacy links)
### US3
- Run in parallel:
- T019: tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php (non-entitled 404)
- T020: tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php (non-member 404)
- T021: tests/Feature/RequiredPermissions/RequiredPermissionsLegacyRouteTest.php
- T022: tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php (no fallback)
---
## Task completeness validation
- Every user story has:
- At least one independently runnable verification test task
- Implementation tasks with concrete file paths
- A clear checkpoint goal and independent test criteria