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.phpand accessed viaGraphClientInterface. - 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 /groupswith$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:
pending→running→succeeded|failed(optionallypartialif 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)