diff --git a/specs/004-assignments-scope-tags/spec.md b/specs/004-assignments-scope-tags/spec.md index ea65ce4..7229547 100644 --- a/specs/004-assignments-scope-tags/spec.md +++ b/specs/004-assignments-scope-tags/spec.md @@ -141,12 +141,14 @@ ### Backup & Storage **FR-004.2**: When assignments are included, system MUST fetch assignments using fallback strategy: 1. Try: `/deviceManagement/configurationPolicies/{id}/assignments` 2. If empty/fails: Try `$expand=assignments` on policy fetch -3. Continue capture with assignments `null` on failure (fail-soft) and set `assignments_fetch_failed: true`. +3. Continue capture with assignments `null` on failure (fail-soft) and set `assignments_fetch_failed: true` in PolicyVersion metadata. + - This flag covers any failure during assignment capture/enrichment (fetch, group resolution, filter resolution). **FR-004.3**: System MUST enrich assignments with: - Group display name + orphaned flag via `/directoryObjects/getByIds` - Assignment filter name via `/deviceManagement/assignmentFilters` - Preserve target type (include/exclude) and filter mode (`deviceAndAppManagementAssignmentFilterType`) +- If filter name lookup fails or filter ID is unknown, keep filter ID + mode, omit the name, and continue capture (UI displays filter ID when name is missing). **FR-004.4**: System MUST store assignments and scope tags on PolicyVersion: - `policy_versions.assignments` (array, nullable) @@ -154,16 +156,7 @@ ### Backup & Storage - hashes for deduplication (`assignments_hash`, `scope_tags_hash`) BackupItem MUST link to PolicyVersion via `policy_version_id` and copy assignments for restore. -**FR-004.5**: Capture metadata stored on PolicyVersion and BackupItem MUST include: -```json -{ - "has_assignments": true, - "has_scope_tags": true, - "has_orphaned_assignments": false, - "assignments_fetch_failed": false -} -``` -Assignment counts are derived from `assignments` at display time. +**FR-004.5**: PolicyVersion metadata MUST include capture flags (see Data Model). BackupItem metadata MAY mirror these flags for display/audit, but PolicyVersion is the source of truth. Assignment counts are derived from `assignments` at display time. ### UI Display @@ -177,9 +170,14 @@ ### UI Display **FR-004.8**: System MUST render orphaned group IDs as "Unknown Group (ID: {id})" with warning icon. +**Terminology**: +- **Orphaned group ID**: A group ID referenced in assignments that cannot be resolved in the source tenant during capture. +- **Unresolved group ID**: A group ID not found in the target tenant during restore mapping. +- UI SHOULD render both as "Unknown Group (ID: ...)" with warning styling. + ### Restore with Group Mapping -**FR-004.9**: Restore preview MUST detect unresolved group IDs by calling POST `/directoryObjects/getByIds` with batch of group IDs extracted from source assignments (types: ["group"]). Missing IDs = unresolved. +**FR-004.9**: Restore preview MUST detect unresolved (target-missing) group IDs by calling POST `/directoryObjects/getByIds` with batch of group IDs extracted from source assignments (types: ["group"]). Missing IDs = unresolved. **FR-004.10**: When unresolved groups exist, system MUST inject a "Group Mapping" step in the restore wizard showing: - Source group (name from backup metadata or ID if name unavailable) @@ -201,6 +199,7 @@ ### Restore with Group Mapping - 201 Created on POST = success - Log request-id/client-request-id on any failure 6. Continue with remaining assignments if one fails (fail-soft) +7. Restore is best-effort: no transactional rollback between DELETE and POST. If DELETE succeeds but POST fails, record a failed outcome, mark the restore as partial, and allow retry. **FR-004.13**: System MUST handle assignment restore failures gracefully: - Log per-assignment outcome (success/skip/failure) @@ -219,9 +218,9 @@ ### Scope Tags **FR-004.16**: System MUST resolve Scope Tag names via `/deviceManagement/roleScopeTags` (with caching, TTL 1 hour). -**FR-004.17**: During restore, system SHOULD preserve Scope Tag IDs if they exist in target tenant, or: -- Log warning if Scope Tag ID doesn't exist in target -- Allow policy creation to proceed (Graph API default behavior) +**FR-004.17**: During restore, system MUST preserve Scope Tag IDs that exist in the target tenant. If a Scope Tag ID is missing: +- Log a warning +- Proceed without that tag (best-effort, Graph API default behavior) **FR-004.18**: Restore preview MUST show Scope Tag diff: "Scope Tags: 2 matched, 1 not found in target tenant". @@ -229,7 +228,7 @@ ### Scope Tags ## Non-Functional Requirements -**NFR-004.1**: Assignment fetching MUST NOT block backup creation (async or fail-soft). +**NFR-004.1**: Assignment fetching MUST NOT block capture actions (Add Policies / Capture Snapshot). Use async or fail-soft behavior. **NFR-004.2**: Group mapping UI MUST support search/filter for tenants with 500+ groups. @@ -289,7 +288,7 @@ ### `policy_versions.scope_tags` JSONB schema } ``` -### `backup_items.metadata` JSONB schema +### `policy_versions.metadata` JSONB schema ```json { @@ -300,6 +299,8 @@ ### `backup_items.metadata` JSONB schema } ``` +BackupItem metadata MAY include the same flags copied from the PolicyVersion for display/audit, but PolicyVersion is the source of truth. + --- ## Graph API Integration @@ -338,7 +339,7 @@ ### Endpoints to Add (Production-Tested Strategies) - **DELETE** `/deviceManagement/configurationPolicies/{id}/assignments/{assignmentId}` - Returns: 204 No Content - - **Restore Strategy**: DELETE all existing assignments, then POST new ones (atomic via transaction pattern) + - **Restore Strategy**: DELETE all existing assignments, then POST new ones (best-effort; record per-assignment outcomes, no transactional rollback) 3. **POST** `/directoryObjects/getByIds` (Stable Group Resolution) - Body: `{ "ids": ["id1", "id2"], "types": ["group"] }`