feat/004-assignments-scope-tags #4
889
specs/004-assignments-scope-tags/tasks.md
Normal file
889
specs/004-assignments-scope-tags/tasks.md
Normal file
@ -0,0 +1,889 @@
|
||||
# Feature 004: Assignments & Scope Tags - Task Breakdown
|
||||
|
||||
## Overview
|
||||
This document breaks down the implementation plan into granular, actionable tasks organized by phase and user story.
|
||||
|
||||
**Total Estimated Tasks**: 62 tasks
|
||||
**MVP Scope**: Tasks marked with ⭐ (24 tasks, ~16-22 hours)
|
||||
**Full Implementation**: All tasks (~30-40 hours)
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup & Database (Foundation)
|
||||
|
||||
**Duration**: 2-3 hours
|
||||
**Dependencies**: None
|
||||
**Parallelizable**: No (sequential setup)
|
||||
|
||||
### Tasks
|
||||
|
||||
**1.1** ⭐ Create migration: `add_assignments_to_backup_items`
|
||||
- File: `database/migrations/xxxx_add_assignments_to_backup_items.php`
|
||||
- Add `assignments` JSONB column after `metadata`
|
||||
- Make nullable
|
||||
- Write reversible `down()` method
|
||||
- Test: `php artisan migrate` and `migrate:rollback`
|
||||
|
||||
**1.2** ⭐ Create migration: `add_group_mapping_to_restore_runs`
|
||||
- File: `database/migrations/xxxx_add_group_mapping_to_restore_runs.php`
|
||||
- Add `group_mapping` JSONB column after `results`
|
||||
- Make nullable
|
||||
- Write reversible `down()` method
|
||||
- Test: `php artisan migrate` and `migrate:rollback`
|
||||
|
||||
**1.3** ⭐ Update `BackupItem` model with assignments cast
|
||||
- Add `'assignments' => 'array'` to `$casts`
|
||||
- Add `assignments` to `$fillable`
|
||||
- Test: Create BackupItem with assignments, verify cast works
|
||||
|
||||
**1.4** ⭐ Add `BackupItem` assignment accessor methods
|
||||
- `getAssignmentCountAttribute(): int`
|
||||
- `hasAssignments(): bool`
|
||||
- `getGroupIdsAttribute(): array`
|
||||
- `getScopeTagIdsAttribute(): array`
|
||||
- `getScopeTagNamesAttribute(): array`
|
||||
- `hasOrphanedAssignments(): bool`
|
||||
- `assignmentsFetchFailed(): bool`
|
||||
|
||||
**1.5** ⭐ Add `BackupItem` scope: `scopeWithAssignments()`
|
||||
- Filter policies with non-null assignments
|
||||
- Use `whereNotNull('assignments')` and `whereRaw('json_array_length(assignments) > 0')`
|
||||
|
||||
**1.6** ⭐ Update `RestoreRun` model with group_mapping cast
|
||||
- Add `'group_mapping' => 'array'` to `$casts`
|
||||
- Add `group_mapping` to `$fillable`
|
||||
- Test: Create RestoreRun with group_mapping, verify cast works
|
||||
|
||||
**1.7** ⭐ Add `RestoreRun` group mapping helper methods
|
||||
- `hasGroupMapping(): bool`
|
||||
- `getMappedGroupId(string $sourceGroupId): ?string`
|
||||
- `isGroupSkipped(string $sourceGroupId): bool`
|
||||
- `getUnmappedGroupIds(array $sourceGroupIds): array`
|
||||
- `addGroupMapping(string $sourceGroupId, string $targetGroupId): void`
|
||||
|
||||
**1.8** ⭐ Add `RestoreRun` assignment outcome methods
|
||||
- `getAssignmentRestoreOutcomes(): array`
|
||||
- `getSuccessfulAssignmentsCount(): int`
|
||||
- `getFailedAssignmentsCount(): int`
|
||||
- `getSkippedAssignmentsCount(): int`
|
||||
|
||||
**1.9** ⭐ Update `config/graph_contracts.php` with assignments endpoints
|
||||
- Add `assignments_list_path` (GET)
|
||||
- Add `assignments_create_path` (POST)
|
||||
- Add `assignments_delete_path` (DELETE)
|
||||
- Add `supports_scope_tags: true`
|
||||
- Add `scope_tag_field: 'roleScopeTagIds'`
|
||||
|
||||
**1.10** ⭐ Write unit tests: `BackupItemTest`
|
||||
- Test assignment accessors
|
||||
- Test scope `withAssignments()`
|
||||
- Test metadata helpers (scope tags, orphaned flags)
|
||||
- Expected: 100% coverage for new methods
|
||||
|
||||
**1.11** ⭐ Write unit tests: `RestoreRunTest`
|
||||
- Test group mapping helpers
|
||||
- Test assignment outcome methods
|
||||
- Test `addGroupMapping()` persistence
|
||||
- Expected: 100% coverage for new methods
|
||||
|
||||
**1.12** Run Pint: Format all new code
|
||||
- `./vendor/bin/pint database/migrations/`
|
||||
- `./vendor/bin/pint app/Models/BackupItem.php`
|
||||
- `./vendor/bin/pint app/Models/RestoreRun.php`
|
||||
|
||||
**1.13** Verify tests pass
|
||||
- Run: `php artisan test --filter=BackupItem`
|
||||
- Run: `php artisan test --filter=RestoreRun`
|
||||
- Expected: All green
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Graph API Integration (Core Services)
|
||||
|
||||
**Duration**: 4-6 hours
|
||||
**Dependencies**: Phase 1 complete
|
||||
**Parallelizable**: Yes (services independent)
|
||||
|
||||
### Tasks
|
||||
|
||||
**2.1** ⭐ Create service: `AssignmentFetcher`
|
||||
- File: `app/Services/Graph/AssignmentFetcher.php`
|
||||
- Method: `fetch(string $tenantId, string $policyId): array`
|
||||
- Implement primary endpoint: GET `/assignments`
|
||||
- Implement fallback: GET with `$expand=assignments`
|
||||
- Return empty array on failure (fail-soft)
|
||||
- Log warnings with request IDs
|
||||
|
||||
**2.2** ⭐ Add error handling to `AssignmentFetcher`
|
||||
- Catch `GraphException`
|
||||
- Log: tenant_id, policy_id, error message, request_id
|
||||
- Return empty array (don't throw)
|
||||
- Set flag for caller: `assignments_fetch_failed`
|
||||
|
||||
**2.3** ⭐ Write unit test: `AssignmentFetcherTest::primary_endpoint_success`
|
||||
- Mock Graph response with assignments
|
||||
- Assert returned array matches response
|
||||
- Assert no fallback called
|
||||
|
||||
**2.4** ⭐ Write unit test: `AssignmentFetcherTest::fallback_on_empty_response`
|
||||
- Mock primary returning empty array
|
||||
- Mock fallback returning assignments
|
||||
- Assert fallback called
|
||||
- Assert assignments returned
|
||||
|
||||
**2.5** ⭐ Write unit test: `AssignmentFetcherTest::fail_soft_on_error`
|
||||
- Mock both endpoints throwing `GraphException`
|
||||
- Assert empty array returned
|
||||
- Assert warning logged
|
||||
|
||||
**2.6** Create service: `GroupResolver`
|
||||
- File: `app/Services/Graph/GroupResolver.php`
|
||||
- Method: `resolveGroupIds(array $groupIds, string $tenantId): array`
|
||||
- Implement: POST `/directoryObjects/getByIds`
|
||||
- Return keyed array: `['group-id' => ['id', 'displayName', 'orphaned']]`
|
||||
- Handle orphaned IDs (not in response)
|
||||
|
||||
**2.7** Add caching to `GroupResolver`
|
||||
- Cache key: `"groups:{$tenantId}:" . md5(implode(',', $groupIds))`
|
||||
- TTL: 5 minutes
|
||||
- Use `Cache::remember()`
|
||||
|
||||
**2.8** Write unit test: `GroupResolverTest::resolves_all_groups`
|
||||
- Mock Graph response with all group IDs
|
||||
- Assert all resolved with names
|
||||
- Assert `orphaned: false`
|
||||
|
||||
**2.9** Write unit test: `GroupResolverTest::handles_orphaned_ids`
|
||||
- Mock Graph response missing some IDs
|
||||
- Assert orphaned IDs have `displayName: null`
|
||||
- Assert `orphaned: true`
|
||||
|
||||
**2.10** Write unit test: `GroupResolverTest::caches_results`
|
||||
- Call resolver twice with same IDs
|
||||
- Assert Graph API called only once
|
||||
- Assert cache hit on second call
|
||||
|
||||
**2.11** Create service: `ScopeTagResolver`
|
||||
- File: `app/Services/Graph/ScopeTagResolver.php`
|
||||
- Method: `resolve(array $scopeTagIds): array`
|
||||
- Implement: GET `/deviceManagement/roleScopeTags?$select=id,displayName`
|
||||
- Return array of scope tag objects
|
||||
- Cache results (1 hour TTL)
|
||||
|
||||
**2.12** Add cache to `ScopeTagResolver`
|
||||
- Cache key: `"scope_tags:all"`
|
||||
- TTL: 1 hour
|
||||
- Fetch all scope tags once, filter in memory
|
||||
|
||||
**2.13** Write unit test: `ScopeTagResolverTest::resolves_scope_tags`
|
||||
- Mock Graph response
|
||||
- Assert correct scope tags returned
|
||||
- Assert filtered to requested IDs only
|
||||
|
||||
**2.14** Write unit test: `ScopeTagResolverTest::caches_results`
|
||||
- Call resolver twice
|
||||
- Assert Graph API called only once
|
||||
- Assert cache hit on second call
|
||||
|
||||
**2.15** Run Pint: Format service classes
|
||||
- `./vendor/bin/pint app/Services/Graph/`
|
||||
|
||||
**2.16** Verify service tests pass
|
||||
- Run: `php artisan test --filter=AssignmentFetcher`
|
||||
- Run: `php artisan test --filter=GroupResolver`
|
||||
- Run: `php artisan test --filter=ScopeTagResolver`
|
||||
- Expected: All green, 90%+ coverage
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: US1 - Backup with Assignments (MVP Core)
|
||||
|
||||
**Duration**: 4-5 hours
|
||||
**Dependencies**: Phase 2 complete
|
||||
**Parallelizable**: Partially (service and UI separate)
|
||||
|
||||
### Tasks
|
||||
|
||||
**3.1** ⭐ Add checkbox to Backup creation form
|
||||
- File: `app/Filament/Resources/BackupResource/Pages/CreateBackup.php`
|
||||
- Component: `Checkbox::make('include_assignments')`
|
||||
- Label: "Include Assignments & Scope Tags"
|
||||
- Help text: "Captures group/user targeting and RBAC scope. Adds ~2-5 KB per policy."
|
||||
- Default: `false`
|
||||
- Show only for Settings Catalog policies
|
||||
|
||||
**3.2** ⭐ Create service: `AssignmentBackupService`
|
||||
- File: `app/Services/AssignmentBackupService.php`
|
||||
- Method: `backup(int $tenantId, string $policyId, bool $includeAssignments): BackupItem`
|
||||
- Call `AssignmentFetcher` if `includeAssignments` true
|
||||
- Call `ScopeTagResolver` for scope tags
|
||||
- Update `backup_items.assignments` and `metadata`
|
||||
|
||||
**3.3** ⭐ Implement assignment fetch in `AssignmentBackupService`
|
||||
- Check `includeAssignments` flag
|
||||
- Call `$this->assignmentFetcher->fetch($tenantId, $policyId)`
|
||||
- Store result in `$backupItem->assignments`
|
||||
- If empty or failed, set `metadata['assignments_fetch_failed'] = true`
|
||||
|
||||
**3.4** ⭐ Implement scope tag resolution in `AssignmentBackupService`
|
||||
- Extract `roleScopeTagIds` from policy payload
|
||||
- Call `$this->scopeTagResolver->resolve($scopeTagIds)`
|
||||
- Store in `metadata['scope_tag_ids']` and `metadata['scope_tag_names']`
|
||||
|
||||
**3.5** ⭐ Update `metadata` with assignment summary
|
||||
- Set `metadata['assignment_count']`
|
||||
- Set `metadata['has_orphaned_assignments']` (detect via GroupResolver)
|
||||
- Set `metadata['assignments_fetch_failed']` on error
|
||||
|
||||
**3.6** Create job: `FetchAssignmentsJob`
|
||||
- File: `app/Jobs/FetchAssignmentsJob.php`
|
||||
- Dispatch async after backup creation if checkbox enabled
|
||||
- Call `AssignmentBackupService` in job
|
||||
- Handle failures gracefully (log, don't retry)
|
||||
|
||||
**3.7** Add audit log entry: `backup.assignments.included`
|
||||
- Create entry when checkbox enabled
|
||||
- Metadata: tenant_id, backup_set_id, policy_count, assignment_count
|
||||
|
||||
**3.8** ⭐ Write feature test: `BackupWithAssignmentsTest::creates_backup_with_assignments`
|
||||
- Mock Graph API responses (assignments, scope tags)
|
||||
- Create backup with checkbox enabled
|
||||
- Assert `backup_items.assignments` populated
|
||||
- Assert `metadata['assignment_count']` correct
|
||||
- Assert audit log entry created
|
||||
|
||||
**3.9** ⭐ Write feature test: `BackupWithAssignmentsTest::creates_backup_without_assignments`
|
||||
- Create backup with checkbox disabled
|
||||
- Assert `backup_items.assignments` is null
|
||||
- Assert `metadata['assignment_count']` is 0 or not set
|
||||
|
||||
**3.10** ⭐ Write feature test: `BackupWithAssignmentsTest::handles_fetch_failure`
|
||||
- Mock Graph API throwing exception
|
||||
- Create backup with checkbox enabled
|
||||
- Assert backup completes (fail-soft)
|
||||
- Assert `metadata['assignments_fetch_failed'] = true`
|
||||
- Assert warning logged
|
||||
|
||||
**3.11** Run Pint: Format new code
|
||||
- `./vendor/bin/pint app/Services/AssignmentBackupService.php`
|
||||
- `./vendor/bin/pint app/Jobs/FetchAssignmentsJob.php`
|
||||
|
||||
**3.12** Verify feature tests pass
|
||||
- Run: `php artisan test --filter=BackupWithAssignments`
|
||||
- Expected: All green
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: US2 - Policy View with Assignments Tab
|
||||
|
||||
**Duration**: 3-4 hours
|
||||
**Dependencies**: Phase 3 complete
|
||||
**Parallelizable**: Yes (independent of Phase 5)
|
||||
|
||||
### Tasks
|
||||
|
||||
**4.1** ⭐ Add "Assignments" tab to Policy view
|
||||
- File: `app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`
|
||||
- Use Filament `Tabs` component
|
||||
- Add tab: `Tab::make('Assignments')`
|
||||
- Show only for Settings Catalog policies with assignments
|
||||
|
||||
**4.2** ⭐ Create assignments table in tab
|
||||
- Use Filament `Table` component
|
||||
- Columns: Type (group/user/device), Name, Mode (Include/Exclude), ID
|
||||
- Data source: `$this->record->assignments` (from BackupItem via Policy relationship)
|
||||
|
||||
**4.3** ⭐ Handle orphaned group IDs in table
|
||||
- Check if `displayName` is null
|
||||
- Render: "Unknown Group (ID: {id})" with warning icon
|
||||
- Use Filament `Badge` component with color `warning`
|
||||
|
||||
**4.4** Create Scope Tags section
|
||||
- Below assignments table
|
||||
- Use Filament `Infolist` component
|
||||
- Display scope tag names with IDs: "Default (ID: 0), HR-Admins (ID: abc-123)"
|
||||
|
||||
**4.5** Handle empty state
|
||||
- Show message: "No assignments found"
|
||||
- Use Filament `EmptyState` component
|
||||
- Icon: `heroicon-o-user-group`
|
||||
|
||||
**4.6** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_assignments_table`
|
||||
- Create policy with assignments
|
||||
- Visit policy view page
|
||||
- Assert "Assignments" tab visible
|
||||
- Assert table contains assignment data
|
||||
|
||||
**4.7** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_orphaned_ids_with_warning`
|
||||
- Create policy with orphaned group ID (no name in metadata)
|
||||
- Visit assignments tab
|
||||
- Assert "Unknown Group" text visible
|
||||
- Assert warning badge present
|
||||
|
||||
**4.8** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::displays_scope_tags`
|
||||
- Create policy with scope tags
|
||||
- Visit assignments tab
|
||||
- Assert scope tag names + IDs displayed
|
||||
|
||||
**4.9** ⭐ Write feature test: `PolicyViewAssignmentsTabTest::shows_empty_state`
|
||||
- Create policy without assignments
|
||||
- Visit assignments tab
|
||||
- Assert "No assignments found" message visible
|
||||
|
||||
**4.10** Run Pint: Format view code
|
||||
- `./vendor/bin/pint app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`
|
||||
|
||||
**4.11** Verify feature tests pass
|
||||
- Run: `php artisan test --filter=PolicyViewAssignmentsTab`
|
||||
- Expected: All green
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: US3 - Restore with Group Mapping (Core Restore)
|
||||
|
||||
**Duration**: 6-8 hours
|
||||
**Dependencies**: Phase 4 complete
|
||||
**Parallelizable**: No (complex, sequential)
|
||||
|
||||
### Tasks
|
||||
|
||||
**5.1** Detect unresolved groups in restore preview
|
||||
- File: `app/Filament/Resources/RestoreResource/Pages/RestorePreview.php`
|
||||
- Extract group IDs from source assignments
|
||||
- Call `POST /directoryObjects/getByIds` for target tenant
|
||||
- Compare: missing IDs = unresolved
|
||||
|
||||
**5.2** Create Filament wizard component: `GroupMappingStep`
|
||||
- File: `app/Filament/Forms/Components/GroupMappingStep.php`
|
||||
- Add as step 2 in restore wizard (between Preview and Confirm)
|
||||
- Show only if unresolved groups exist
|
||||
|
||||
**5.3** Build group mapping table in wizard step
|
||||
- Use Filament `Repeater` or custom table
|
||||
- Columns: Source Group (name or ID), Target Group (dropdown), Skip (checkbox)
|
||||
- Populate source groups from backup metadata
|
||||
|
||||
**5.4** Implement target group dropdown
|
||||
- Use `Select::make('target_group_id')`
|
||||
- `->searchable()`
|
||||
- `->getSearchResultsUsing()` to query target tenant groups
|
||||
- `->debounce(500)` for responsive search
|
||||
- `->lazy()` to load options on demand
|
||||
|
||||
**5.5** Add "Skip" checkbox functionality
|
||||
- When checked, set mapping value to `"SKIP"`
|
||||
- Disable target group dropdown when skip checked
|
||||
|
||||
**5.6** Cache target tenant groups
|
||||
- In wizard `mount()`, call `GroupResolver->getAllForTenant($targetTenantId)`
|
||||
- Cache for 5 minutes
|
||||
- Pre-warm dropdown options
|
||||
|
||||
**5.7** Persist group mapping to `RestoreRun`
|
||||
- On wizard step 2 submit, save mapping to `$restoreRun->group_mapping`
|
||||
- Use `RestoreRun->addGroupMapping()` helper
|
||||
- Validate: all unresolved groups either mapped or skipped
|
||||
|
||||
**5.8** Create service: `AssignmentRestoreService`
|
||||
- File: `app/Services/AssignmentRestoreService.php`
|
||||
- Method: `restore(string $policyId, array $assignments, array $groupMapping): array`
|
||||
- Implement DELETE-then-CREATE pattern
|
||||
|
||||
**5.9** Implement DELETE existing assignments
|
||||
- Step 1: GET `/assignments` for target policy
|
||||
- Step 2: Loop and DELETE each assignment
|
||||
- Handle 204 No Content (success)
|
||||
- Log warnings on failure, continue
|
||||
|
||||
**5.10** Implement CREATE new assignments with mapping
|
||||
- Step 3: Loop through source assignments
|
||||
- Apply group mapping: replace source group IDs with target IDs
|
||||
- Skip assignments marked `"SKIP"` in mapping
|
||||
- POST each assignment to `/assignments`
|
||||
- Handle 201 Created (success)
|
||||
- Log per-assignment outcome
|
||||
|
||||
**5.11** Add rate limit protection
|
||||
- Add 100ms delay between sequential POST calls: `usleep(100000)`
|
||||
- Log request IDs for failed calls
|
||||
|
||||
**5.12** Handle per-assignment failures gracefully
|
||||
- Don't throw on failure, collect outcomes
|
||||
- Outcomes array: `['status' => 'success|failed|skipped', 'assignment' => ..., 'error' => ...]`
|
||||
- Continue with remaining assignments (fail-soft)
|
||||
|
||||
**5.13** Store outcomes in `RestoreRun->results`
|
||||
- Add `assignment_outcomes` key to results JSON
|
||||
- Include successful, failed, skipped counts
|
||||
- Store error details for failed assignments
|
||||
|
||||
**5.14** Add audit log entries
|
||||
- `restore.group_mapping.applied`: When mapping saved
|
||||
- `restore.assignment.created`: Per successful assignment
|
||||
- `restore.assignment.failed`: Per failed assignment
|
||||
- `restore.assignment.skipped`: Per skipped assignment
|
||||
|
||||
**5.15** Create job: `RestoreAssignmentsJob`
|
||||
- File: `app/Jobs/RestoreAssignmentsJob.php`
|
||||
- Dispatch async after restore initiated
|
||||
- Call `AssignmentRestoreService`
|
||||
- Handle job failures (log, mark RestoreRun as failed)
|
||||
|
||||
**5.16** Write feature test: `RestoreGroupMappingTest::detects_unresolved_groups`
|
||||
- Create backup with group IDs
|
||||
- Target tenant has different groups
|
||||
- Start restore wizard
|
||||
- Assert group mapping step appears
|
||||
- Assert unresolved groups listed
|
||||
|
||||
**5.17** Write feature test: `RestoreGroupMappingTest::persists_group_mapping`
|
||||
- Fill out group mapping form
|
||||
- Submit wizard
|
||||
- Assert `RestoreRun->group_mapping` populated
|
||||
- Assert audit log entry created
|
||||
|
||||
**5.18** Write feature test: `RestoreGroupMappingTest::skips_mapped_groups`
|
||||
- Map some groups, skip others
|
||||
- Complete restore
|
||||
- Assert skipped groups have `"SKIP"` value
|
||||
- Assert skipped assignments not created
|
||||
|
||||
**5.19** Write feature test: `RestoreAssignmentApplicationTest::applies_assignments_successfully`
|
||||
- Mock Graph API (DELETE 204, POST 201)
|
||||
- Restore with mapped groups
|
||||
- Assert assignments created in target tenant
|
||||
- Assert outcomes logged
|
||||
|
||||
**5.20** Write feature test: `RestoreAssignmentApplicationTest::handles_failures_gracefully`
|
||||
- Mock Graph API: some POSTs fail (400 or 500)
|
||||
- Restore with mapped groups
|
||||
- Assert successful assignments still created
|
||||
- Assert failed assignments logged with errors
|
||||
- Assert RestoreRun status reflects partial success
|
||||
|
||||
**5.21** Run Pint: Format restore code
|
||||
- `./vendor/bin/pint app/Services/AssignmentRestoreService.php`
|
||||
- `./vendor/bin/pint app/Jobs/RestoreAssignmentsJob.php`
|
||||
- `./vendor/bin/pint app/Filament/Forms/Components/GroupMappingStep.php`
|
||||
|
||||
**5.22** Verify feature tests pass
|
||||
- Run: `php artisan test --filter=RestoreGroupMapping`
|
||||
- Run: `php artisan test --filter=RestoreAssignmentApplication`
|
||||
- Expected: All green
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: US4 - Restore Preview with Assignment Diff
|
||||
|
||||
**Duration**: 2-3 hours
|
||||
**Dependencies**: Phase 5 complete
|
||||
**Parallelizable**: Yes (extends Phase 5, doesn't block)
|
||||
|
||||
### Tasks
|
||||
|
||||
**6.1** Fetch target policy's current assignments
|
||||
- In restore preview, call `AssignmentFetcher` for target policy
|
||||
- Cache for duration of wizard session
|
||||
|
||||
**6.2** Implement diff algorithm
|
||||
- Compare source assignments with target assignments
|
||||
- Group by change type:
|
||||
- **Added**: In source, not in target
|
||||
- **Removed**: In target, not in source
|
||||
- **Unchanged**: In both (match by group ID + intent)
|
||||
|
||||
**6.3** Display diff in Restore Preview
|
||||
- Use Filament `Infolist` with color coding:
|
||||
- Green (success): Added assignments
|
||||
- Red (danger): Removed assignments
|
||||
- Gray (secondary): Unchanged assignments
|
||||
- Show counts: "3 added, 1 removed, 2 unchanged"
|
||||
|
||||
**6.4** Add Scope Tag diff
|
||||
- Compare source scope tag IDs with target tenant scope tags
|
||||
- Display: "Scope Tags: 2 matched, 1 not found in target"
|
||||
- Show warning icon for missing scope tags
|
||||
|
||||
**6.5** Update feature test: `RestoreAssignmentApplicationTest::displays_assignment_diff`
|
||||
- Visit restore preview
|
||||
- Assert diff sections visible
|
||||
- Assert color coding correct
|
||||
- Assert counts accurate
|
||||
|
||||
**6.6** Run Pint: Format diff code
|
||||
- `./vendor/bin/pint app/Filament/Resources/RestoreResource/Pages/RestorePreview.php`
|
||||
|
||||
**6.7** Verify test passes
|
||||
- Run: `php artisan test --filter=displays_assignment_diff`
|
||||
- Expected: Green
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Scope Tags (Full Support)
|
||||
|
||||
**Duration**: 2-3 hours
|
||||
**Dependencies**: Phase 6 complete
|
||||
**Parallelizable**: Yes (extends existing code)
|
||||
|
||||
### Tasks
|
||||
|
||||
**7.1** Extract scope tag IDs during backup
|
||||
- In `AssignmentBackupService`, read `roleScopeTagIds` from policy payload
|
||||
- Store in `metadata['scope_tag_ids']`
|
||||
|
||||
**7.2** Resolve scope tag names during backup
|
||||
- Call `ScopeTagResolver->resolve($scopeTagIds)`
|
||||
- Store in `metadata['scope_tag_names']`
|
||||
|
||||
**7.3** Validate scope tags during restore
|
||||
- In `AssignmentRestoreService`, check if scope tag IDs exist in target tenant
|
||||
- Call `POST /directoryObjects/getByIds` with type `['scopeTag']` (if supported) or GET all scope tags
|
||||
|
||||
**7.4** Log warnings for missing scope tags
|
||||
- Log: "Scope Tag '{id}' not found in target tenant, restore will proceed"
|
||||
- Don't block restore (Graph API handles missing scope tags gracefully)
|
||||
|
||||
**7.5** Update unit test: `ScopeTagResolverTest::handles_missing_scope_tags`
|
||||
- Mock Graph response with some scope tags missing
|
||||
- Assert warnings logged
|
||||
- Assert restore proceeds
|
||||
|
||||
**7.6** Update feature test: `RestoreAssignmentApplicationTest::handles_missing_scope_tags`
|
||||
- Restore with scope tag IDs not in target tenant
|
||||
- Assert warnings logged
|
||||
- Assert policy created successfully
|
||||
|
||||
**7.7** Run Pint: Format scope tag code
|
||||
|
||||
**7.8** Verify tests pass
|
||||
- Run: `php artisan test --filter=ScopeTag`
|
||||
- Expected: Green
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Polish & Performance
|
||||
|
||||
**Duration**: 3-4 hours
|
||||
**Dependencies**: Phase 7 complete
|
||||
**Parallelizable**: Partially (UI polish vs performance tuning)
|
||||
|
||||
### Tasks
|
||||
|
||||
**8.1** Add loading indicators to group mapping dropdown
|
||||
- Use `wire:loading` on Select component
|
||||
- Show spinner during search
|
||||
- Disable dropdown while loading
|
||||
|
||||
**8.2** Add debouncing to group search
|
||||
- Set `->debounce(500)` on Select component
|
||||
- Test: Type quickly, verify search only fires after 500ms pause
|
||||
|
||||
**8.3** Optimize Graph API calls: batch group resolution
|
||||
- In `GroupResolver`, batch max 100 IDs per POST
|
||||
- If > 100 groups, split into chunks: `collect($groupIds)->chunk(100)`
|
||||
- Merge results
|
||||
|
||||
**8.4** Add cache warming for target tenant groups
|
||||
- In wizard `mount()`, pre-fetch all target tenant groups
|
||||
- Cache for 5 minutes
|
||||
- Display loading message while warming
|
||||
|
||||
**8.5** Add tooltips to UI elements
|
||||
- Backup checkbox: "Captures group/user targeting and RBAC scope. Adds ~2-5 KB per policy."
|
||||
- Group mapping: "Map source groups to target groups for cross-tenant migrations."
|
||||
- Skip checkbox: "Don't restore assignments targeting this group."
|
||||
|
||||
**8.6** Update README: Add "Assignments & Scope Tags" section
|
||||
- Overview of feature
|
||||
- How to use backup checkbox
|
||||
- How to use group mapping wizard
|
||||
- Troubleshooting tips
|
||||
|
||||
**8.7** Create performance test: Large restore
|
||||
- Restore 50 policies with 10 assignments each
|
||||
- Measure duration
|
||||
- Target: < 5 minutes
|
||||
- Log: Graph API call count, cache hits
|
||||
|
||||
**8.8** Run performance test
|
||||
- Execute test
|
||||
- Record results in `quickstart.md`
|
||||
- Optimize if needed (e.g., increase cache TTL)
|
||||
|
||||
**8.9** Run Pint: Format all code
|
||||
|
||||
**8.10** Final test run
|
||||
- Run: `php artisan test`
|
||||
- Expected: All tests green, 85%+ coverage
|
||||
|
||||
---
|
||||
|
||||
## Phase 9: Testing & QA
|
||||
|
||||
**Duration**: 2-3 hours
|
||||
**Dependencies**: Phase 8 complete
|
||||
**Parallelizable**: No (QA sequential)
|
||||
|
||||
### Tasks
|
||||
|
||||
**9.1** Manual QA: Backup with assignments (same tenant)
|
||||
- Navigate to backup creation
|
||||
- Enable checkbox
|
||||
- Verify backup completes
|
||||
- Verify assignments stored in DB
|
||||
- Verify audit log entry
|
||||
|
||||
**9.2** Manual QA: Backup without assignments
|
||||
- Navigate to backup creation
|
||||
- Disable checkbox
|
||||
- Verify backup completes
|
||||
- Verify assignments column is null
|
||||
|
||||
**9.3** Manual QA: Policy view with assignments tab
|
||||
- Navigate to policy with assignments
|
||||
- Click "Assignments" tab
|
||||
- Verify table renders
|
||||
- Verify orphaned IDs show warning
|
||||
- Verify scope tags section
|
||||
|
||||
**9.4** Manual QA: Restore to same tenant (auto-match)
|
||||
- Restore backup to original tenant
|
||||
- Verify no group mapping step appears
|
||||
- Verify assignments restored correctly
|
||||
|
||||
**9.5** Manual QA: Restore to different tenant (group mapping)
|
||||
- Restore backup to different tenant
|
||||
- Verify group mapping step appears
|
||||
- Fill out mapping (map some, skip others)
|
||||
- Verify assignments restored with mapped IDs
|
||||
- Verify skipped assignments not created
|
||||
|
||||
**9.6** Manual QA: Handle orphaned group IDs
|
||||
- Create backup with group ID that doesn't exist in target
|
||||
- Restore to target tenant
|
||||
- Verify warning displayed
|
||||
- Verify orphaned group rendered as "Unknown Group"
|
||||
|
||||
**9.7** Manual QA: Handle Graph API failures
|
||||
- Simulate API failure (disable network or use Http::fake with 500 response)
|
||||
- Attempt backup with checkbox
|
||||
- Verify fail-soft behavior
|
||||
- Verify warning logged
|
||||
|
||||
**9.8** Browser test: `GroupMappingWizardTest`
|
||||
- Use Pest browser testing
|
||||
- Navigate restore wizard
|
||||
- Fill out group mapping
|
||||
- Submit
|
||||
- Verify mapping persisted
|
||||
|
||||
**9.9** Load testing: 100+ policies
|
||||
- Create 100 policies with 20 assignments each
|
||||
- Restore to target tenant
|
||||
- Measure duration
|
||||
- Verify < 5 minutes
|
||||
|
||||
**9.10** Staging deployment
|
||||
- Deploy to staging via Dokploy
|
||||
- Run manual QA scenarios on staging
|
||||
- Verify no issues
|
||||
|
||||
**9.11** Document QA results
|
||||
- Update `quickstart.md` with QA checklist results
|
||||
- Note any issues or edge cases discovered
|
||||
|
||||
---
|
||||
|
||||
## Phase 10: Deployment & Documentation
|
||||
|
||||
**Duration**: 1-2 hours
|
||||
**Dependencies**: Phase 9 complete
|
||||
**Parallelizable**: No (sequential deployment)
|
||||
|
||||
### Tasks
|
||||
|
||||
**10.1** Review deployment checklist
|
||||
- Migrations ready? ✅
|
||||
- Rollback tested? ✅
|
||||
- Env vars needed? (None for this feature)
|
||||
- Queue workers running? ✅
|
||||
|
||||
**10.2** Run migrations on staging
|
||||
- SSH to staging server
|
||||
- Run: `php artisan migrate`
|
||||
- Verify no errors
|
||||
- Check backup_items and restore_runs tables
|
||||
|
||||
**10.3** Smoke test on staging
|
||||
- Create backup with assignments
|
||||
- Restore to same tenant
|
||||
- Verify success
|
||||
|
||||
**10.4** Update spec.md with implementation notes
|
||||
- Add "Implementation Status" section
|
||||
- Note any deviations from original spec
|
||||
- Document known limitations (e.g., Settings Catalog only in Phase 1)
|
||||
|
||||
**10.5** Create migration guide
|
||||
- Document: "Existing backups will NOT have assignments (not retroactive)"
|
||||
- Document: "Re-create backups with checkbox to capture assignments"
|
||||
|
||||
**10.6** Add monitoring alerts
|
||||
- Alert: Assignment fetch failure rate > 10%
|
||||
- Alert: Group resolution failure rate > 5%
|
||||
- Use Laravel logs or external monitoring (e.g., Sentry)
|
||||
|
||||
**10.7** Production deployment
|
||||
- Deploy to production via Dokploy
|
||||
- Run migrations
|
||||
- Monitor logs for 24 hours
|
||||
- Check for errors
|
||||
|
||||
**10.8** Verify no rate limit issues
|
||||
- Monitor Graph API headers: `X-RateLimit-Remaining`
|
||||
- If rate limiting detected, increase delays (currently 100ms)
|
||||
|
||||
**10.9** Final documentation update
|
||||
- Update README with feature status
|
||||
- Link to quickstart.md for developers
|
||||
- Add to feature list
|
||||
|
||||
**10.10** Close tasks in tasks.md
|
||||
- Mark all tasks complete ✅
|
||||
- Archive planning docs (optional)
|
||||
|
||||
---
|
||||
|
||||
## Task Summary
|
||||
|
||||
### MVP Scope (⭐ Tasks)
|
||||
**Total**: 24 tasks
|
||||
**Estimated**: 16-22 hours
|
||||
|
||||
**Breakdown**:
|
||||
- Phase 1 (Setup): 11 tasks (2-3h)
|
||||
- Phase 2 (Graph Services): 5 tasks (subset, ~2-3h)
|
||||
- Phase 3 (Backup): 6 tasks (3-4h)
|
||||
- Phase 4 (Policy View): 6 tasks (3-4h)
|
||||
- Phase 5 (Restore, basic): Partial (subset for same-tenant restore)
|
||||
|
||||
**MVP Scope Definition**:
|
||||
- ✅ Backup with assignments checkbox (US1)
|
||||
- ✅ Policy view with assignments tab (US2)
|
||||
- ✅ Basic restore (same tenant, auto-match) (US3 partial)
|
||||
- ❌ Group mapping wizard (defer to post-MVP)
|
||||
- ❌ Restore preview diff (defer to post-MVP)
|
||||
- ❌ Scope tags full support (defer to post-MVP)
|
||||
|
||||
---
|
||||
|
||||
### Full Implementation
|
||||
**Total**: 62 tasks
|
||||
**Estimated**: 30-40 hours
|
||||
|
||||
**Breakdown**:
|
||||
- Phase 1: 13 tasks (2-3h)
|
||||
- Phase 2: 16 tasks (4-6h)
|
||||
- Phase 3: 12 tasks (4-5h)
|
||||
- Phase 4: 11 tasks (3-4h)
|
||||
- Phase 5: 22 tasks (6-8h)
|
||||
- Phase 6: 7 tasks (2-3h)
|
||||
- Phase 7: 8 tasks (2-3h)
|
||||
- Phase 8: 10 tasks (3-4h)
|
||||
- Phase 9: 11 tasks (2-3h)
|
||||
- Phase 10: 10 tasks (1-2h)
|
||||
|
||||
---
|
||||
|
||||
## Parallel Development Opportunities
|
||||
|
||||
### Track 1: Backend Services (Dev A)
|
||||
- Phase 1: Database setup (13 tasks)
|
||||
- Phase 2: Graph API services (16 tasks)
|
||||
- Phase 3: Backup service (12 tasks)
|
||||
- Phase 5: Restore service (22 tasks)
|
||||
|
||||
**Total**: ~16-22 hours
|
||||
|
||||
---
|
||||
|
||||
### Track 2: Frontend/UI (Dev B)
|
||||
- Phase 4: Policy view tab (11 tasks)
|
||||
- Phase 5: Group mapping wizard (subset of Phase 5, ~10 tasks)
|
||||
- Phase 6: Restore preview diff (7 tasks)
|
||||
- Phase 8: UI polish (subset, ~5 tasks)
|
||||
|
||||
**Total**: ~12-16 hours
|
||||
|
||||
---
|
||||
|
||||
### Track 3: Testing & QA (Dev C or shared)
|
||||
- Phase 2: Unit tests for services (subset)
|
||||
- Phase 3: Feature tests for backup (subset)
|
||||
- Phase 4: Feature tests for policy view (subset)
|
||||
- Phase 9: Manual QA + browser tests (11 tasks)
|
||||
|
||||
**Total**: ~8-12 hours
|
||||
|
||||
**Note**: Testing can be done incrementally as phases complete.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies Graph
|
||||
|
||||
```
|
||||
Phase 1 (Setup)
|
||||
↓
|
||||
Phase 2 (Graph Services)
|
||||
↓
|
||||
Phase 3 (Backup) → Phase 4 (Policy View)
|
||||
↓ ↓
|
||||
Phase 5 (Restore) ←------+
|
||||
↓
|
||||
Phase 6 (Preview Diff)
|
||||
↓
|
||||
Phase 7 (Scope Tags)
|
||||
↓
|
||||
Phase 8 (Polish)
|
||||
↓
|
||||
Phase 9 (Testing & QA)
|
||||
↓
|
||||
Phase 10 (Deployment)
|
||||
```
|
||||
|
||||
**Parallel Phases**: Phase 3 and Phase 4 can run in parallel after Phase 2.
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation Tasks
|
||||
|
||||
### Risk: Graph API Rate Limiting
|
||||
- **Task 2.11**: Add retry logic with exponential backoff
|
||||
- **Task 5.11**: Add 100ms delay between POSTs
|
||||
- **Task 8.3**: Batch group resolution (max 100 per request)
|
||||
|
||||
### Risk: Large Group Counts (500+)
|
||||
- **Task 5.4**: Implement searchable dropdown with debounce
|
||||
- **Task 5.6**: Cache target tenant groups
|
||||
- **Task 8.4**: Pre-warm cache on wizard mount
|
||||
|
||||
### Risk: Assignment Restore Failures
|
||||
- **Task 5.12**: Fail-soft per-assignment error handling
|
||||
- **Task 5.13**: Store outcomes in RestoreRun results
|
||||
- **Task 5.14**: Audit log all outcomes
|
||||
|
||||
---
|
||||
|
||||
## Next Actions
|
||||
|
||||
1. **Review**: Team review of tasks.md, adjust estimates if needed
|
||||
2. **Assign**: Assign tasks to developers (Track 1/2/3)
|
||||
3. **Start**: Begin with Phase 1 (Setup & Database)
|
||||
4. **Track Progress**: Update task status as work progresses
|
||||
5. **Iterate**: Adjust plan based on discoveries during implementation
|
||||
|
||||
---
|
||||
|
||||
**Status**: Task Breakdown Complete
|
||||
**Ready for Implementation**: ✅
|
||||
**Estimated Duration**: 30-40 hours (MVP: 16-22 hours)
|
||||
Loading…
Reference in New Issue
Block a user