From b9c47e79735b0dfc4973b450dd2d37ba262249b3 Mon Sep 17 00:00:00 2001 From: ahmido Date: Thu, 25 Dec 2025 14:25:16 +0000 Subject: [PATCH] feat/006-sot-foundations-assignments (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ## Spec-Driven Development (SDD) - [ ] Es gibt eine Spec unter `specs/-/` - [ ] Enthaltene Dateien: `plan.md`, `tasks.md`, `spec.md` - [ ] Spec beschreibt Verhalten/Acceptance Criteria (nicht nur Implementation) - [ ] Wenn sich Anforderungen während der Umsetzung geändert haben: Spec/Plan/Tasks wurden aktualisiert ## Implementation - [ ] Implementierung entspricht der Spec - [ ] Edge cases / Fehlerfälle berücksichtigt - [ ] Keine unbeabsichtigten Änderungen außerhalb des Scopes ## Tests - [ ] Tests ergänzt/aktualisiert (Pest/PHPUnit) - [ ] Relevante Tests lokal ausgeführt (`./vendor/bin/sail artisan test` oder `php artisan test`) ## Migration / Config / Ops (falls relevant) - [ ] Migration(en) enthalten und getestet - [ ] Rollback bedacht (rückwärts kompatibel, sichere Migration) - [ ] Neue Env Vars dokumentiert (`.env.example` / Doku) - [ ] Queue/cron/storage Auswirkungen geprüft ## UI (Filament/Livewire) (falls relevant) - [ ] UI-Flows geprüft - [ ] Screenshots/Notizen hinzugefügt ## Notes Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/6 --- .../checklists/requirements.md | 36 ++++++ .../assignment-apply-request.schema.json | 34 ++++++ .../contracts/foundation-snapshot.schema.json | 18 +++ .../restore-mapping-report.schema.json | 33 ++++++ .../data-model.md | 111 ++++++++++++++++++ specs/006-sot-foundations-assignments/plan.md | 108 +++++++++++++++++ .../quickstart.md | 55 +++++++++ .../research.md | 86 ++++++++++++++ specs/006-sot-foundations-assignments/spec.md | 92 +++++++++++++++ 9 files changed, 573 insertions(+) create mode 100644 specs/006-sot-foundations-assignments/checklists/requirements.md create mode 100644 specs/006-sot-foundations-assignments/contracts/assignment-apply-request.schema.json create mode 100644 specs/006-sot-foundations-assignments/contracts/foundation-snapshot.schema.json create mode 100644 specs/006-sot-foundations-assignments/contracts/restore-mapping-report.schema.json create mode 100644 specs/006-sot-foundations-assignments/data-model.md create mode 100644 specs/006-sot-foundations-assignments/plan.md create mode 100644 specs/006-sot-foundations-assignments/quickstart.md create mode 100644 specs/006-sot-foundations-assignments/research.md create mode 100644 specs/006-sot-foundations-assignments/spec.md diff --git a/specs/006-sot-foundations-assignments/checklists/requirements.md b/specs/006-sot-foundations-assignments/checklists/requirements.md new file mode 100644 index 0000000..3f7b940 --- /dev/null +++ b/specs/006-sot-foundations-assignments/checklists/requirements.md @@ -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. diff --git a/specs/006-sot-foundations-assignments/contracts/assignment-apply-request.schema.json b/specs/006-sot-foundations-assignments/contracts/assignment-apply-request.schema.json new file mode 100644 index 0000000..0ab92a7 --- /dev/null +++ b/specs/006-sot-foundations-assignments/contracts/assignment-apply-request.schema.json @@ -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 +} diff --git a/specs/006-sot-foundations-assignments/contracts/foundation-snapshot.schema.json b/specs/006-sot-foundations-assignments/contracts/foundation-snapshot.schema.json new file mode 100644 index 0000000..0e2a90d --- /dev/null +++ b/specs/006-sot-foundations-assignments/contracts/foundation-snapshot.schema.json @@ -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 +} diff --git a/specs/006-sot-foundations-assignments/contracts/restore-mapping-report.schema.json b/specs/006-sot-foundations-assignments/contracts/restore-mapping-report.schema.json new file mode 100644 index 0000000..c9ba3f3 --- /dev/null +++ b/specs/006-sot-foundations-assignments/contracts/restore-mapping-report.schema.json @@ -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 +} diff --git a/specs/006-sot-foundations-assignments/data-model.md b/specs/006-sot-foundations-assignments/data-model.md new file mode 100644 index 0000000..d9927c5 --- /dev/null +++ b/specs/006-sot-foundations-assignments/data-model.md @@ -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": "", + "sourceName": "Filter A", + "decision": "mapped_existing|created|created_copy|failed", + "targetId": "", + "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. diff --git a/specs/006-sot-foundations-assignments/plan.md b/specs/006-sot-foundations-assignments/plan.md new file mode 100644 index 0000000..be28388 --- /dev/null +++ b/specs/006-sot-foundations-assignments/plan.md @@ -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 + + + +**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) + + +```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] | diff --git a/specs/006-sot-foundations-assignments/quickstart.md b/specs/006-sot-foundations-assignments/quickstart.md new file mode 100644 index 0000000..8b23863 --- /dev/null +++ b/specs/006-sot-foundations-assignments/quickstart.md @@ -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. diff --git a/specs/006-sot-foundations-assignments/research.md b/specs/006-sot-foundations-assignments/research.md new file mode 100644 index 0000000..a13d6c5 --- /dev/null +++ b/specs/006-sot-foundations-assignments/research.md @@ -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). diff --git a/specs/006-sot-foundations-assignments/spec.md b/specs/006-sot-foundations-assignments/spec.md new file mode 100644 index 0000000..a65bcd9 --- /dev/null +++ b/specs/006-sot-foundations-assignments/spec.md @@ -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.