TenantAtlas/specs/112-list-expand-parity/plan.md
ahmido 32c3a64147 feat(112): LIST $expand parity + Entra principal names (#136)
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
2026-02-25 23:54:20 +00:00

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 in config/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 OperationRun usage; 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:

  • $expand normalization 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.md
  • specs/112-list-expand-parity/contracts/graph-client-listPolicies-options.schema.json
  • specs/112-list-expand-parity/quickstart.md

Agent context update:

  • Run .specify/scripts/bash/update-agent-context.sh copilot after Phase 1 artifacts are current.

Phase 2 — Implementation Plan (execute next)

  1. Update GraphContractRegistry::sanitizeQuery()
  • Normalize $expand input:
    • Accept string|string[].
    • If string: split on top-level commas only (ignore commas inside balanced parentheses); trim whitespace.
  • De-dupe tokens; drop empty.
  • Enforce maxTokenLen=200 (drop tokens exceeding the cap).
  • Filter via allowed_expand exact-match allowlist (no partial matching).
  • Enforce maxItems=10 after allowlist (keep first N allowed).
  • Emit diagnostics (warn in non-prod, debug in prod) when tokens are removed/truncated.
  1. Update MicrosoftGraphClient::listPolicies()
  • Accept expand in $options and forward it into $queryInput as '$expand'.
  • Preserve existing behavior when expand is not provided.
  1. Fix Entra Admin Roles report
  • In EntraAdminRolesReportService::fetchRoleAssignments(), pass expand => 'principal' alongside resolved tenant graph options.
  • Keep the current fallback behavior (Unknown) for true upstream missing-data cases.
  1. Improve discoverability of list query options
  • Expand PHPDoc on GraphClientInterface::listPolicies() (and align with getPolicy() docs) to describe supported keys and the expand input shape.
  1. 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 $expand is present in the outbound query.
  • EntraAdminRolesReportServiceTest: ensure principal display names are used when returned, and ensure the graph client is invoked with expand=principal.
  1. Validate
  • vendor/bin/sail bin pint --dirty --format agent
  • Run the focused tests listed in specs/112-list-expand-parity/quickstart.md.