feat(006): foundations + assignment mapping and preview-only restore guard #7

Merged
ahmido merged 8 commits from feat/006-sot-foundations-assignments into dev 2025-12-26 23:44:32 +00:00
9 changed files with 573 additions and 0 deletions
Showing only changes of commit d14ad7b525 - Show all commits

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: SoT Foundations & Assignments
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2025-12-25
**Feature**: [specs/006-sot-foundations-assignments/spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
- Validation pass: Spec contains no [NEEDS CLARIFICATION] markers and scopes CA restore to preview-only until dependency mapping exists.

View File

@ -0,0 +1,34 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "assignment-apply-request.schema.json",
"title": "AssignmentApplyRequest",
"type": "object",
"required": ["assignments"],
"properties": {
"assignments": {
"type": "array",
"items": {
"type": "object",
"required": ["target"],
"properties": {
"target": {
"type": "object",
"required": ["@odata.type"],
"properties": {
"@odata.type": { "type": "string" },
"groupId": { "type": "string" },
"deviceAndAppManagementAssignmentFilterId": { "type": "string" },
"deviceAndAppManagementAssignmentFilterType": {
"type": "string",
"enum": ["include", "exclude", "Include", "Exclude"]
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
}

View File

@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "foundation-snapshot.schema.json",
"title": "FoundationSnapshot",
"type": "object",
"required": ["type", "sourceId", "payload"],
"properties": {
"type": {
"type": "string",
"enum": ["assignmentFilter", "roleScopeTag", "notificationMessageTemplate"]
},
"sourceId": { "type": "string" },
"displayName": { "type": "string" },
"payload": { "type": "object" },
"metadata": { "type": ["object", "null"] }
},
"additionalProperties": false
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "restore-mapping-report.schema.json",
"title": "RestoreMappingReport",
"type": "object",
"required": ["foundations"],
"properties": {
"foundations": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "sourceId", "decision"],
"properties": {
"type": {
"type": "string",
"enum": ["assignmentFilter", "roleScopeTag", "notificationMessageTemplate"]
},
"sourceId": { "type": "string" },
"sourceName": { "type": "string" },
"decision": {
"type": "string",
"enum": ["mapped_existing", "created", "created_copy", "skipped", "failed"]
},
"targetId": { "type": ["string", "null"] },
"targetName": { "type": ["string", "null"] },
"reason": { "type": ["string", "null"] }
},
"additionalProperties": false
}
}
},
"additionalProperties": true
}

View File

@ -0,0 +1,111 @@
# Data Model: SoT Foundations & Assignments (006)
This feature reuses existing snapshot and restore run entities, and introduces a consistent JSON “mapping + decisions” report.
## Existing Entities (today)
### BackupSet
- Purpose: Groups a point-in-time capture for a tenant.
- Relationships: hasMany `BackupItem`.
### BackupItem
- Purpose: Stores an immutable snapshot item.
- Key fields (relevant):
- `tenant_id`, `backup_set_id`
- `policy_id` (nullable)
- `policy_identifier` (Graph id)
- `policy_type` (logical type)
- `payload` (raw JSON)
- `metadata` (normalized JSON)
### RestoreRun
- Purpose: Tracks restore preview/execution lifecycle.
- Key fields (relevant):
- `is_dry_run`
- `requested_items` (selection)
- `preview` (dry-run decision report)
- `results` (execution report)
- `metadata` (extra structured info)
## New / Extended Concepts (this feature)
### FoundationSnapshot (logical concept)
Represented as a `backup_items` row.
- `policy_type` (new keys):
- `assignmentFilter`
- `roleScopeTag`
- `notificationMessageTemplate`
- `policy_identifier`: source Graph `id`
- `policy_id`: `null`
- `payload`: raw Graph resource JSON
- `metadata` (proposed, shape):
```json
{
"displayName": "...",
"kind": "assignmentFilter|roleScopeTag|notificationMessageTemplate",
"graph": {
"resource": "deviceManagement/assignmentFilters",
"apiVersion": "v1.0"
}
}
```
### RestoreMappingReport (logical concept)
Stored within `restore_runs.preview`/`restore_runs.results`.
- `mappings.foundations[]` (proposed shape):
```json
{
"type": "assignmentFilter",
"sourceId": "<old-guid>",
"sourceName": "Filter A",
"decision": "mapped_existing|created|created_copy|failed",
"targetId": "<new-guid>",
"targetName": "Filter A (Copy)",
"reason": "..."
}
```
### AssignmentDecisionReport (logical concept)
Stored within `restore_runs.preview`/`restore_runs.results`.
- `assignments[]` entries (proposed shape):
```json
{
"policyType": "settingsCatalogPolicy",
"sourcePolicyId": "...",
"targetPolicyId": "...",
"decision": "applied|skipped|failed",
"reason": "missing_filter_mapping|missing_group_mapping|preview_only|graph_error",
"details": {
"sourceAssignmentCount": 3,
"appliedAssignmentCount": 2
}
}
```
## Relationships / Flow
- `BackupSet` contains both “policy snapshots” and “foundation snapshots” as `BackupItem` rows.
- `RestoreRun` consumes a `BackupSet` and produces:
- foundation mapping report
- policy restore decisions
- assignment application decisions
## Validation & State Transitions
- Restore execution is single-writer per tenant (existing safety requirement FR-009).
- Restore behavior:
- Preview (`is_dry_run=true`): builds mapping/decisions, **no Graph writes**.
- Execute (`is_dry_run=false`): creates missing foundations, restores policies, applies assignments when safe.
- Conditional Access entries are always recorded as preview-only/skipped in execute.

View File

@ -0,0 +1,108 @@
# Implementation Plan: SoT Foundations & Assignments
**Branch**: `006-sot-foundations-assignments` | **Date**: 2025-12-25 | **Spec**: ./spec.md
**Input**: Feature specification from `/specs/006-sot-foundations-assignments/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
Implement foundations-first backup/restore for Intune dependencies (Assignment Filters, Scope Tags, Notification Message Templates) and extend restore to be assignment-aware using a deterministic old→new ID mapping report. Conditional Access remains preview-only (never executed) until its dependency mapping is supported.
Phase outputs:
- Phase 0 research: `./research.md`
- Phase 1 design: `./data-model.md`, `./contracts/`, `./quickstart.md`
## Technical Context
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**Language/Version**: PHP 8.4 (Laravel 12)
**Primary Dependencies**: Laravel 12, Filament v4, Livewire v3, Microsoft Graph (custom client abstraction)
**Storage**: PostgreSQL (JSONB payload storage for snapshots)
**Testing**: Pest v4 + PHPUnit 12
**Target Platform**: Docker/Sail locally; container deploy via Dokploy
**Project Type**: Web application (Laravel backend + Filament admin UI)
**Performance Goals**: Restore preview for ~100 items in <2 minutes (SC-003); handle Graph paging and throttling safely
**Constraints**: Restore must be defensive: no deletions; skip unsafe assignments; produce audit/report; respect Graph throttling
**Scale/Scope**: Tenants with large policy inventories; focus on foundational object types + assignment application for already-supported policy types
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
The constitution at `.specify/memory/constitution.md` is currently an unfilled template (no ratified gates). For this feature, adopt the repos documented operating rules as gates:
- **Sail-first** local dev/test commands.
- **SpecKit Gate Rule**: code changes must be accompanied by `specs/006-sot-foundations-assignments/` updates.
- **Testing is required**: every behavioral change covered by Pest tests.
- **Safety**: restore never deletes; assignments only applied when mapped; CA stays preview-only.
- **Auditability**: restore/backup outcomes recorded and tenant-scoped.
If the team later ratifies a real constitution, re-map these gates accordingly.
**Post-Phase 1 re-check**: Pass (no violations introduced by the Phase 1 design artifacts).
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── 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)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
app/
├── Filament/
│ └── Resources/
├── Jobs/
├── Models/
│ ├── BackupItem.php
│ ├── BackupSet.php
│ └── RestoreRun.php
├── Services/
│ ├── Graph/
│ └── Intune/
└── Support/
config/
├── graph_contracts.php
└── tenantpilot.php
database/
├── migrations/
└── factories/
tests/
├── Feature/
└── Unit/
```
**Structure Decision**: Implement as incremental additions to existing Laravel services/models/jobs, with Filament UI using the existing Backup/Restore flows.
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |

View File

@ -0,0 +1,55 @@
# Quickstart: SoT Foundations & Assignments (006)
This is a developer/operator checklist to validate foundations-first restore and assignment-aware restore.
## Prerequisites
- Local dev via Sail.
- A tenant configured for Graph access with sufficient permissions for:
- Assignment filters: `DeviceManagementConfiguration.ReadWrite.All`
- Scope tags: `DeviceManagementRBAC.ReadWrite.All`
- Notification templates: `DeviceManagementServiceConfig.ReadWrite.All`
## Scenario A: Foundations backup + restore
1. In a test tenant, create:
- 12 assignment filters
- 12 scope tags (non-built-in)
- 1 notification message template
2. Run a sync + backup via the apps existing workflow.
3. In the target tenant, ensure those objects do not exist.
4. Run restore in **preview**:
- Verify preview includes a “Foundations” section.
- Verify it reports old→new mapping decisions.
5. Run restore in **execute**:
- Verify missing foundations are created.
- Verify collisions result in “created_copy” behavior (if you intentionally create same-named items beforehand).
## Scenario B: Assignment-aware restore
1. Create a policy that has assignments:
- Group targeting
- Assignment filters (include/exclude)
- Scope tags where applicable
2. Back up the tenant.
3. Restore into a target tenant where:
- some foundations exist
- some foundations are missing
4. Run restore preview:
- Verify assignments are marked “applied” only when mappings exist.
- Verify unsafe assignments are “skipped” with explicit reasons (no broad targeting).
5. Run restore execute:
- Verify the policy is restored.
- Verify assignment application uses the mapping.
## Scenario C: Conditional Access preview-only
1. Ensure the backup contains at least one Conditional Access policy.
2. Run restore preview:
- Verify CA items appear with a clear preview-only marker.
3. Run restore execute:
- Verify CA changes are not applied and are recorded as skipped/preview-only.
## Notes
- If UI changes dont appear, run the projects dev/build pipeline (`composer run dev` / `pnpm dev`) according to existing repo conventions.

View File

@ -0,0 +1,86 @@
# Research: SoT Foundations & Assignments (006)
This document resolves planning unknowns and records decisions for implementing foundations-first backup/restore and assignment-aware restore.
## Decision: Foundation object endpoints and permissions
- **Decision**: Implement “foundation” backup/restore for:
- Assignment Filters via `deviceManagement/assignmentFilters` (permission: `DeviceManagementConfiguration.ReadWrite.All`).
- Scope Tags via `deviceManagement/roleScopeTags` (permission: `DeviceManagementRBAC.ReadWrite.All`).
- Notification Message Templates via `deviceManagement/notificationMessageTemplates` (permission: `DeviceManagementServiceConfig.ReadWrite.All`, with `localizedNotificationMessages` treated as a future enhancement).
- **Rationale**: These are explicitly called out as SoT foundations and appear as dependencies in the IntuneManagement reference implementation.
- **Alternatives considered**:
- Treat foundations as “manual prerequisites” only (no backup/restore) → rejected because it blocks safe assignment restore.
- Store only names (no full payload) → rejected because restore needs full object definitions.
## Decision: Assignment apply mechanism (Graph)
- **Decision**: Apply assignments using a per-resource `.../{id}/assign` Graph action (default), with request body shape:
```json
{
"assignments": [
{
"target": {
"@odata.type": "...",
"groupId": "...",
"deviceAndAppManagementAssignmentFilterId": "...",
"deviceAndAppManagementAssignmentFilterType": "Include|Exclude"
}
}
]
}
```
and support type-specific overrides if needed.
- **Rationale**: Matches the IntuneManagement import approach and aligns with SoT “apply assignments after foundations exist”.
- **Alternatives considered**:
- PATCH the resource with an `assignments` property → rejected because many Intune resources do not support assignment updates via PATCH.
- Only restore object payloads, never assignments → rejected (SoT requires assignment-aware restore).
## Decision: Mapping strategy (deterministic, safe)
- **Decision**: Produce and persist an “old → new” mapping for foundation objects by matching primarily on `displayName` (or name-equivalent), with collision handling:
- If a unique match exists in the target tenant by name: reuse (map old → existing).
- If no match exists: create (map old → created).
- If multiple matches exist: create a copy with a predictable suffix and record “created_copy” in the report.
- **Rationale**: SoT requires determinism and auditability; mapping by opaque IDs is impossible across tenants.
- **Alternatives considered**:
- Always create new objects regardless of matches → rejected due to duplication and name collision risk.
- Hash-based matching (normalize and compare multiple fields) → deferred; start with name-based plus explicit collision handling.
## Decision: Where to store mappings and restore decision report
- **Decision**: Store mapping + decisions in `restore_runs.preview` (dry-run) and `restore_runs.results` (execute), optionally mirrored into `restore_runs.metadata` for fast access.
- **Rationale**: The schema already supports JSON `preview`/`results`; this keeps the first iteration simple and audit-friendly.
- **Alternatives considered**:
- Dedicated `restore_mappings` table → deferred until querying/reporting requirements demand it.
## Decision: How to represent foundation snapshots in storage
- **Decision**: Store foundation snapshots as `backup_items` rows with:
- `policy_id = null`
- `policy_type` set to a dedicated type key (e.g. `assignmentFilter`, `roleScopeTag`, `notificationMessageTemplate`)
- `policy_identifier` set to the Graph object `id`
- `payload` containing the raw Graph resource
- `metadata` containing normalized identifiers used for matching (e.g. `displayName`).
- **Rationale**: `backup_items.policy_id` is nullable; reusing the same snapshot container avoids schema churn.
- **Alternatives considered**:
- New “foundation_snapshots” table → rejected for MVP due to extra migrations and duplication.
## Decision: Conditional Access restore behavior (safety)
- **Decision**: Keep Conditional Access restore as **preview-only**, even in execute mode.
- **Rationale**: CA depends on identity objects (e.g., named locations) and is security-critical; SoT explicitly allows preview-first for risky items.
- **Alternatives considered**:
- Allow CA restore with best-effort group/user mapping → rejected as too risky without complete dependency mapping.
## Decision: Scope for assignment-aware restore (initial)
- **Decision**: Apply assignment mapping for existing supported configuration objects (policy types already in `config/tenantpilot.php`), focusing first on targets that include:
- group targeting (`groupId`)
- assignment filters (`deviceAndAppManagementAssignmentFilterId`/Type)
- role scope tags (`roleScopeTagIds`) where applicable.
- **Rationale**: Incrementally delivers value without requiring support for all object classes in SoT.
- **Alternatives considered**:
- Expand to named locations / terms of use / authentication strengths immediately → deferred (separate dependency set).

View File

@ -0,0 +1,92 @@
# Feature Specification: SoT Foundations & Assignments
**Feature Branch**: `006-sot-foundations-assignments`
**Created**: 2025-12-25
**Status**: Draft
**Input**: User description: "SoT Foundations & Assignments: implement backup/restore foundations (assignment filters, scope tags, notification templates) and add assignment-aware backup/restore pipeline with ID mapping for core Intune objects; keep Conditional Access restore preview-only until named locations/mapping exist."
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Restore Foundations First (Priority: P1)
As an admin, I want to back up and restore the core "foundation" objects that other configurations depend on (assignment filters, scope tags, and compliance notification templates), so that later restores can reliably re-apply assignments and dependencies.
**Why this priority**: Without these foundations, restores either fail or must skip assignments/dependencies, which reduces trust and makes outcomes unpredictable.
**Independent Test**: In a test tenant with at least one filter, one scope tag, and one notification template: create a backup snapshot, then restore into a tenant where they are missing. Verify that the restored objects exist and that a mapping from old IDs to new IDs is produced.
**Acceptance Scenarios**:
1. **Given** a tenant with assignment filters, **When** a backup is created and later restored into a tenant missing those filters, **Then** missing filters are created and the restore reports the old→new identifier mapping.
2. **Given** a tenant with scope tags, **When** a restore runs, **Then** scope tags are restored before any dependent objects are applied.
3. **Given** a tenant with compliance notification templates, **When** a restore runs, **Then** templates are restored before applying compliance policy scheduled actions.
---
### User Story 2 - Apply Assignments Safely (Priority: P2)
As an admin, I want restores to apply assignments for supported configuration objects using the foundation mappings, so that a restore reproduces intended targeting while staying safe and auditable.
**Why this priority**: Restoring payloads without assignments is incomplete; restoring assignments without safe mapping can be dangerous.
**Independent Test**: Restore a small set of supported configurations that include assignments with filters and scope tags. Verify that assignments are applied when mappings exist, and skipped with a clear reason when mappings are missing.
**Acceptance Scenarios**:
1. **Given** a configuration object whose assignments reference filters/scope tags that exist (or can be mapped), **When** restore executes, **Then** assignments are applied and reported as applied.
2. **Given** a configuration object whose assignments reference a missing dependency (e.g., an unknown filter), **When** restore executes, **Then** the assignment is skipped (not broadly applied) and a human-readable reason is recorded.
3. **Given** an object restore with name collisions, **When** the system cannot unambiguously match a target, **Then** it creates a copy with a predictable suffix and records this decision in the restore report.
---
### User Story 3 - Conditional Access Stays Preview-Only (Priority: P3)
As an admin, I want to preview Conditional Access (CA) policies and their dependencies, but I do not want CA restore to execute automatically until dependency mapping is supported.
**Why this priority**: CA is security-critical and often depends on other objects (like named locations) and identity references. A preview still delivers value without risking outages.
**Independent Test**: Include CA policies in a backup and run restore in "preview" mode. Verify preview shows intended actions and highlights missing dependencies, while execute mode does not apply CA changes.
**Acceptance Scenarios**:
1. **Given** a backup containing CA policies, **When** a restore preview is generated, **Then** CA items appear in preview with a clear "preview-only" indicator.
2. **Given** a restore execution (non-dry-run), **When** CA items are included, **Then** the system does not apply CA changes and records them as preview-only/skipped.
### Edge Cases
- Missing permissions: backup/restore continues for other object types and clearly reports which categories failed due to permissions.
- Name collisions: multiple objects share the same display name; system must avoid ambiguous updates.
- Missing identity references: group/user references cannot be resolved; system must skip the assignment and report.
- Large tenants: operations must cope with pagination and partial failures without losing auditability.
- Throttling/transient failures: system retries safely and produces a final report if some items could not be processed.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: System MUST support backup and restore of foundation objects: assignment filters, scope tags, and compliance notification templates.
- **FR-002**: System MUST restore foundation objects before applying any dependent configurations.
- **FR-003**: System MUST produce an identifier mapping report (old→new) for restored foundation objects.
- **FR-004**: System MUST apply assignments for supported configurations using the identifier mapping.
- **FR-005**: System MUST skip assignments that cannot be safely mapped (e.g., missing dependencies) and MUST record a clear skip reason.
- **FR-006**: System MUST be able to run in preview mode that produces the same decision report as execute mode, without making changes.
- **FR-007**: System MUST NOT delete objects in the target tenant as part of restore.
- **FR-008**: System MUST record an audit trail for backup and restore actions, including outcomes, partial failures, and skipped items.
- **FR-009**: System MUST prevent conflicting simultaneous restore executions for the same tenant (single-writer safety).
- **FR-010**: System MUST keep Conditional Access restore as preview-only until dependency mapping for CA is supported.
### Key Entities *(include if feature involves data)*
- **Foundation Object Snapshot**: A captured representation of an assignment filter, scope tag, or notification template.
- **Assignment Snapshot**: Captured targeting rules associated with a configuration object.
- **Restore Mapping**: A mapping of source identifiers to newly created target identifiers.
- **Restore Report**: A structured outcome summary containing applied items, skipped items, reasons, and any created copies.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: In a tenant with at least 10 foundation objects, a full foundations restore completes with ≥ 99% of items either applied or explicitly skipped with a reason.
- **SC-002**: For supported configuration objects with assignments, ≥ 95% of assignments are either applied correctly or skipped with a clear reason (no silent failures).
- **SC-003**: Restore preview generation for 100 selected items completes in under 2 minutes in a typical admin environment.
- **SC-004**: Admins can complete a restore workflow (preview → execute) with no ambiguous outcomes: every selected item ends in Applied / Created Copy / Skipped / Failed with a recorded reason.