TenantAtlas/specs/051-entra-group-directory-cache/plan.md
ahmido bc846d7c5c 051-entra-group-directory-cache (#57)
Summary

Adds a tenant-scoped Entra Groups “Directory Cache” to enable DB-only group name resolution across the app (no render-time Graph calls), plus sync runs + observability.

What’s included
	•	Entra Groups cache
	•	New entra_groups storage (tenant-scoped) for group metadata (no memberships).
	•	Retention semantics: groups become stale / retained per spec (no hard delete on first miss).
	•	Group Sync Runs
	•	New “Group Sync Runs” UI (list + detail) with tenant isolation (403 on cross-tenant access).
	•	Manual “Sync Groups” action: creates/reuses a run, dispatches job, DB notification with “View run” link.
	•	Scheduled dispatcher command wired in console.php.
	•	DB-only label resolution (US3)
	•	Shared EntraGroupLabelResolver with safe fallback Unresolved (…last8) and UUID guarding.
	•	Refactors to prefer cached names (no typeahead / no live Graph) in:
	•	Tenant RBAC group selects
	•	Policy version assignments widget
	•	Restore results + restore wizard group mapping labels

Safety / Guardrails
	•	No render-time Graph calls: fail-hard guard test verifies UI paths don’t call GraphClientInterface during page render.
	•	Tenant isolation & authorization: policies + scoped queries enforced (cross-tenant access returns 403, not 404).
	•	Data minimization: only group metadata is cached (no membership/owners).

Tests / Verification
	•	Added/updated tests under tests/Feature/DirectoryGroups and tests/Unit/DirectoryGroups:
	•	Start sync → run record + job dispatch + upserts
	•	Retention purge semantics
	•	Scheduled dispatch wiring
	•	Render-time Graph guard
	•	UI/resource access isolation
	•	Ran:
	•	./vendor/bin/pint --dirty
	•	./vendor/bin/sail artisan test tests/Feature/DirectoryGroups
	•	./vendor/bin/sail artisan test tests/Unit/DirectoryGroups

Notes / Follow-ups
	•	UI polish remains (picker/lookup UX, consistent progress widget/toasts across modules, navigation grouping).
	•	pr-gate checklist still has non-blocking open items (mostly UX/ops polish); requirements gate is green.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #57
2026-01-11 23:24:12 +00:00

5.9 KiB

Implementation Plan: Entra Group Directory Cache (Groups v1)

Branch: 051-entra-group-directory-cache | Date: 2026-01-11 | Spec: specs/051-entra-group-directory-cache/spec.md Input: Feature specification from /specs/051-entra-group-directory-cache/spec.md

Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.

Summary

Provide a tenant-scoped Entra ID Groups metadata cache (no membership/owners) populated by queued sync runs (manual + scheduled), so the admin UI and other modules can resolve group IDs to friendly names without making live directory calls during page render.

Technical Context

Language/Version: PHP 8.4 (Laravel 12) Primary Dependencies: Filament v4, Livewire v3, Microsoft Graph integration via internal GraphClientInterface Storage: PostgreSQL Testing: Pest v4 Target Platform: Web application (Sail-first locally, Dokploy-first deploy) Project Type: web Performance Goals: background sync handles large tenants via paging; UI list/search remains responsive with DB indexes Constraints: no live directory calls during page render; app-only Graph auth; safe error summaries; strict tenant isolation; idempotent runs; retention purge after 90 days last-seen Scale/Scope: full-tenant group enumeration; plan for up to ~100k groups/tenant for v1

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • Inventory-first: cache is “last observed” directory state; no snapshot payloads.
  • Read/write separation: read-only to the directory; only internal DB writes.
  • Graph contract path: directory group list endpoint/fields/permissions must be added to config/graph_contracts.php and accessed via GraphClientInterface.
  • Deterministic capabilities: selection identifier for groups sync is deterministic (stable per tenant + groups-v1).
  • Tenant isolation: all cached groups + run records tenant-scoped; no cross-tenant access paths.
  • Automation: manual + scheduled sync runs use locks/idempotency, run records, safe error codes; handle 429/503 with backoff+jitter.
  • Data minimization: store group metadata only (no membership/owners); logs contain no secrets/tokens.

Gate status (pre-Phase 0): PASS (no violations). Re-check after Phase 1 design.

Gate status (post-Phase 1): PASS (design artifacts present: research.md, data-model.md, contracts/*, quickstart.md).

Project Structure

Documentation (this feature)

specs/051-entra-group-directory-cache/
├── 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/
│   └── Resources/
├── Jobs/
├── Models/
├── Services/
│   ├── Graph/
│   └── Directory/
└── Support/

config/
├── graph.php
└── graph_contracts.php

database/
└── migrations/

routes/
└── web.php

tests/
├── Feature/
└── Unit/

Structure Decision: Laravel web application. Implement directory group sync + cache as tenant-scoped Models, Services, Jobs, Filament pages/resources, migrations, and Pest tests.

Key Implementation Decisions (Pinned)

  • Schedule default: daily at 02:00 UTC (environment default) + manual sync always available.
  • Auth mode: app-only (service principal). UI must not require delegated tokens.
  • Permission: Group.Read.All (application).
  • Graph strategy (v1): GET /groups with $select=id,displayName,groupTypes,securityEnabled,mailEnabled + paging via @odata.nextLink.
  • Retention: stale threshold default 30 days; purge after 90 days since last_seen_at.
  • Render safety: fail-hard test ensuring no Graph client calls during render.

Execution Model

Sync Run Lifecycle

  • Run statuses: pendingrunningsucceeded | failed (optionally partial if we intentionally record partial completion).
  • Each run records: initiator (nullable for scheduled), timestamps, observed/upserted/error counters, and a safe error summary + category.

Idempotency & Concurrency

  • Deterministic selection key for v1: groups-v1:all.
  • Deduplication rule: at most one active run per tenant + selection key.
  • Scheduled dispatcher must be idempotent per tenant and schedule slot (no duplicate run creation).

Error Categorization

  • Supported categories: permission, throttling, transient, unknown.
  • Store only safe-to-display summaries (no secrets/tokens).

Definition of Done (per phase)

Phase 2 (Foundational)

  • Migrations created for EntraGroup and EntraGroupSyncRun.
  • Models + factories exist and are tenant-scoped.
  • Graph contract registry has a groups directory read contract.

Phase 3 (US1)

  • Manual sync creates a run and dispatches a job.
  • Job pages through Graph, upserts groups, updates counters, and enforces retention purge.
  • Filament run list/detail exists; scheduler dispatch is wired.
  • Pest tests for run creation, upsert behavior, retention purge, and scheduled run dispatch are green.

Phase 4 (US2)

  • Directory → Groups list/detail exists with search + stale filter + type filter.
  • All browsing renders from DB cache only.

Phase 5 (US3)

  • Shared label resolver resolves from DB cache and provides consistent fallback formatting.
  • Key pages render friendly labels without Graph calls.
  • Fail-hard render guard test is green.

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

N/A (no constitution violations anticipated)