feat(006): foundations + assignment mapping and preview-only restore guard #7
@ -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.
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
111
specs/006-sot-foundations-assignments/data-model.md
Normal file
111
specs/006-sot-foundations-assignments/data-model.md
Normal 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.
|
||||||
108
specs/006-sot-foundations-assignments/plan.md
Normal file
108
specs/006-sot-foundations-assignments/plan.md
Normal 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 repo’s 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] |
|
||||||
55
specs/006-sot-foundations-assignments/quickstart.md
Normal file
55
specs/006-sot-foundations-assignments/quickstart.md
Normal 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:
|
||||||
|
- 1–2 assignment filters
|
||||||
|
- 1–2 scope tags (non-built-in)
|
||||||
|
- 1 notification message template
|
||||||
|
2. Run a sync + backup via the app’s 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 don’t appear, run the project’s dev/build pipeline (`composer run dev` / `pnpm dev`) according to existing repo conventions.
|
||||||
86
specs/006-sot-foundations-assignments/research.md
Normal file
86
specs/006-sot-foundations-assignments/research.md
Normal 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).
|
||||||
92
specs/006-sot-foundations-assignments/spec.md
Normal file
92
specs/006-sot-foundations-assignments/spec.md
Normal 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.
|
||||||
Loading…
Reference in New Issue
Block a user