Implements LIST `$expand` parity with GET by forwarding caller-provided, contract-allowlisted expands. Key changes: - Entra Admin Roles scan now requests `expand=principal` for role assignments so `principal.displayName` can render. - `$expand` normalization/sanitization: top-level comma split (commas inside balanced parentheses preserved), trim, dedupe, allowlist exact match, caps (max 10 tokens, max 200 chars/token). - Diagnostics when expands are removed/truncated (non-prod warning, production low-noise). Tests: - Adds/extends unit coverage for Graph contract sanitization, list request shaping, and the EntraAdminRolesReportService. Spec artifacts included under `specs/112-list-expand-parity/`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #136
5.6 KiB
Implementation Plan: Graph Contracts — LIST $expand Parity Fix
Branch: 112-list-expand-parity | Date: 2026-02-25 | Spec: specs/112-list-expand-parity/spec.md
Input: Feature specification from specs/112-list-expand-parity/spec.md
Summary
Bring LIST query capability parity with GET by forwarding caller-provided, contract-allowlisted $expand on GraphClientInterface::listPolicies(). Improve $expand normalization (top-level comma split + trim + dedupe) and enforce safety caps. Fix the Entra Admin Roles report by explicitly requesting expand=principal for entraRoleAssignments, allowing principal display names to render correctly.
Technical Context
Language/Version: PHP 8.4.x
Primary Dependencies: Laravel 12, custom Microsoft Graph integration, Filament v5 (Livewire v4 compliant)
Storage: PostgreSQL (Sail) — no schema changes for this feature
Testing: Pest v4 (vendor/bin/sail artisan test --compact)
Target Platform: Web application (Laravel + Filament)
Project Type: web
Performance Goals: Prevent pathological query option inputs (bounded $expand)
Constraints: Backwards compatible; no behavior change unless expand is explicitly provided
Scale/Scope: Tenant-scoped Graph reads; low fan-out but called from reports
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
PASS — no violations expected.
- Read/write separation: This feature is read-only (query shape change only).
- Single Contract Path to Graph: Graph calls remain through
GraphClientInterface; allowlists remain inconfig/graph_contracts.php. - Tenant isolation: No cross-tenant reads; only changes outbound query parameters for already-tenant-scoped calls.
- Deterministic capabilities: Allowlist is exact-match and test-covered; no implicit expansions.
- Ops/Observability: No new
OperationRunusage; diagnostics are logs only and low-noise in production.
Re-check post-design: PASS.
Project Structure
Documentation (this feature)
specs/112-list-expand-parity/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── graph-client-listPolicies-options.schema.json
└── tasks.md
Source Code (repository root)
app/
├── Services/
│ ├── EntraAdminRoles/
│ │ └── EntraAdminRolesReportService.php
│ └── Graph/
│ ├── GraphClientInterface.php
│ ├── GraphContractRegistry.php
│ ├── GraphLogger.php
│ ├── GraphResponse.php
│ └── MicrosoftGraphClient.php
config/
└── graph_contracts.php
tests/
├── Unit/
│ ├── GraphContractRegistryTest.php
│ ├── MicrosoftGraphClientListPoliciesSelectTest.php
│ └── EntraAdminRolesReportServiceTest.php
Structure Decision: Single Laravel web application. No new packages/modules.
Phase 0 — Outline & Research (complete)
See specs/112-list-expand-parity/research.md.
Key resolved clarifications / decisions:
$expandnormalization supports string + array inputs.- String inputs split on top-level commas only (commas inside balanced parentheses are not separators).
- Safety caps: max 10 allowlisted expand tokens; max 200 chars per token.
- Diagnostics: warning in non-prod, debug in prod, structured context.
Phase 1 — Design & Contracts (complete)
Artifacts:
specs/112-list-expand-parity/data-model.mdspecs/112-list-expand-parity/contracts/graph-client-listPolicies-options.schema.jsonspecs/112-list-expand-parity/quickstart.md
Agent context update:
- Run
.specify/scripts/bash/update-agent-context.sh copilotafter Phase 1 artifacts are current.
Phase 2 — Implementation Plan (execute next)
- Update
GraphContractRegistry::sanitizeQuery()
- Normalize
$expandinput:- Accept
string|string[]. - If string: split on top-level commas only (ignore commas inside balanced parentheses); trim whitespace.
- Accept
- De-dupe tokens; drop empty.
- Enforce
maxTokenLen=200(drop tokens exceeding the cap). - Filter via
allowed_expandexact-match allowlist (no partial matching). - Enforce
maxItems=10after allowlist (keep first N allowed). - Emit diagnostics (warn in non-prod, debug in prod) when tokens are removed/truncated.
- Update
MicrosoftGraphClient::listPolicies()
- Accept
expandin$optionsand forward it into$queryInputas'$expand'. - Preserve existing behavior when
expandis not provided.
- Fix Entra Admin Roles report
- In
EntraAdminRolesReportService::fetchRoleAssignments(), passexpand => 'principal'alongside resolved tenant graph options. - Keep the current fallback behavior (
Unknown) for true upstream missing-data cases.
- Improve discoverability of list query options
- Expand PHPDoc on
GraphClientInterface::listPolicies()(and align withgetPolicy()docs) to describe supported keys and theexpandinput shape.
- Add regression tests (Pest)
GraphContractRegistryTest: add cases for top-level comma splitting (including commas inside parentheses), dedupe, cap behavior, and non-prod diagnostic log emission.MicrosoftGraphClientListPoliciesSelectTest: add assertions that an allowlisted LIST$expandis present in the outbound query.EntraAdminRolesReportServiceTest: ensure principal display names are used when returned, and ensure the graph client is invoked withexpand=principal.
- Validate
vendor/bin/sail bin pint --dirty --format agent- Run the focused tests listed in
specs/112-list-expand-parity/quickstart.md.