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

128 lines
5.6 KiB
Markdown

# 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)
```text
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)
```text
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.
2) Update `MicrosoftGraphClient::listPolicies()`
- Accept `expand` in `$options` and forward it into `$queryInput` as `'$expand'`.
- Preserve existing behavior when `expand` is not provided.
3) 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.
4) 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.
5) 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`.
6) Validate
- `vendor/bin/sail bin pint --dirty --format agent`
- Run the focused tests listed in `specs/112-list-expand-parity/quickstart.md`.