Archive 5 phases (36 plans) to milestones/v1.0-phases/. Archive roadmap, requirements, and audit to milestones/. Evolve PROJECT.md with shipped state and validated requirements. Collapse ROADMAP.md to one-line milestone summary. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
phase, verified, status, score, human_verification
| phase | verified | status | score | human_verification | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-permissions | 2026-04-02T14:30:00Z | human_needed | 6/7 must-haves verified automatically |
|
Phase 2: Permissions Verification Report
Phase Goal: Implement the Permissions tab — a full SharePoint permissions scanner with multi-site support, CSV/HTML export, and scan options. Port of the PowerShell Generate-PnPSitePermissionRpt function. Verified: 2026-04-02T14:30:00Z Status: human_needed Re-verification: No — initial verification
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | User can scan permissions on a single SharePoint site with configurable depth (PERM-01) | VERIFIED | PermissionsService.ScanSiteAsync fully implemented; SiteUrl bound in XAML; ScanOptions(FolderDepth) passed through |
| 2 | User can scan permissions across multiple selected sites in one operation (PERM-02) | VERIFIED | PermissionsViewModel.RunOperationAsync loops over SelectedSites; PermissionsViewModelTests.StartScanAsync_WithMultipleSiteUrls_CallsServiceOncePerUrl passes; SiteListService + SitePickerDialog wired end-to-end |
| 3 | Permissions scan includes owners, members, guests, external users, and broken inheritance (PERM-03) | VERIFIED | PermissionsService scans site collection admins, web, lists, folders; #EXT# detection in PermissionEntryHelper.IsExternalUser; PrincipalType set correctly; 7 classification tests pass |
| 4 | User can choose to include or exclude inherited permissions (PERM-04) | VERIFIED | IncludeInherited bool bound in XAML via {Binding IncludeInherited}; passed to ScanOptions; ExtractPermissionsAsync skips non-unique objects when IncludeInherited=false |
| 5 | User can export permissions report to CSV (PERM-05) | VERIFIED | CsvExportService.BuildCsv + WriteAsync implemented; UTF-8 BOM; merges rows by (Users, PermissionLevels, GrantedThrough); all 3 CsvExportServiceTests pass |
| 6 | User can export permissions report to interactive HTML (PERM-06) | VERIFIED | HtmlExportService.BuildHtml produces self-contained HTML with inline CSS/JS, stats cards, type badges, external-user pills; all 3 HtmlExportServiceTests pass |
| 7 | SharePoint 5,000-item list view threshold handled via pagination — no silent failures (PERM-07) | VERIFIED | PermissionsService.GetFolderPermissionsAsync uses SharePointPaginationHelper.GetAllItemsAsync with RowLimit 500 pagination — never raw list enumeration (grep confirmed line 222) |
Score: 7/7 truths verified (all automated; 2 items need human confirmation for UI/i18n quality)
Required Artifacts
| Artifact | Provided By | Status | Details |
|---|---|---|---|
SharepointToolbox/Core/Models/PermissionEntry.cs |
Plan 02 | VERIFIED | 9-field record; compiles; referenced by tests |
SharepointToolbox/Core/Models/ScanOptions.cs |
Plan 02 | VERIFIED | Immutable record with correct defaults |
SharepointToolbox/Core/Models/SiteInfo.cs |
Plan 03 | VERIFIED | record SiteInfo(string Url, string Title) |
SharepointToolbox/Services/IPermissionsService.cs |
Plan 02 | VERIFIED | Interface with ScanSiteAsync signature |
SharepointToolbox/Services/PermissionsService.cs |
Plan 02 | VERIFIED | 341 lines; implements all 5 scan paths |
SharepointToolbox/Services/ISiteListService.cs |
Plan 03 | VERIFIED | Interface with GetSitesAsync signature |
SharepointToolbox/Services/SiteListService.cs |
Plan 03 | VERIFIED | DeriveAdminUrl implemented; error wrapping present |
SharepointToolbox/Services/Export/CsvExportService.cs |
Plan 04 | VERIFIED | Merge logic + RFC 4180 escaping + UTF-8 BOM |
SharepointToolbox/Services/Export/HtmlExportService.cs |
Plan 04 | VERIFIED | Self-contained HTML; no external deps; external-user class |
SharepointToolbox/Core/Helpers/PermissionEntryHelper.cs |
Plan 01 | VERIFIED | IsExternalUser, FilterPermissionLevels, IsSharingLinksGroup |
SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs |
Plan 06 | VERIFIED | FeatureViewModelBase subclass; 309 lines; all commands present |
SharepointToolbox/Views/Dialogs/SitePickerDialog.xaml.cs |
Plan 06 | VERIFIED | Loads sites via ISiteListService; filter; CheckBox; OK/Cancel |
SharepointToolbox/Views/Tabs/PermissionsView.xaml |
Plan 07 | VERIFIED | Left config panel + right DataGrid + StatusBar; localized |
SharepointToolbox/Views/Tabs/PermissionsView.xaml.cs |
Plan 07 | VERIFIED | DI wiring; GetRequiredService<PermissionsViewModel>(); dialog factory |
SharepointToolbox/App.xaml.cs |
Plan 07 | VERIFIED | All Phase 2 DI registrations present; Func<TenantProfile, SitePickerDialog> factory registered |
SharepointToolbox/MainWindow.xaml |
Plan 07 | VERIFIED | PermissionsTabItem uses x:Name; no FeatureTabBase stub |
SharepointToolbox/MainWindow.xaml.cs |
Plan 07 | VERIFIED | PermissionsTabItem.Content = serviceProvider.GetRequiredService<PermissionsView>() |
SharepointToolbox/Localization/Strings.resx |
Plan 05 | VERIFIED | 15 Phase 2 keys present (grp.scan.opts, btn.gen.perms, perm.sites.selected, etc.) |
SharepointToolbox/Localization/Strings.fr.resx |
Plan 05 | VERIFIED | 15 keys with French translations (e.g., "Options d'analyse", "Analyser les dossiers") |
SharepointToolbox/Localization/Strings.Designer.cs |
Plan 05 | PARTIAL | 15 new static properties present; tab_permissions property absent (key exists in resx, MainWindow binds via TranslationSource directly — low impact) |
| Test scaffold (5 files) | Plan 01 | VERIFIED | All exist; classification tests pass; ViewModel test passes |
Key Link Verification
| From | To | Via | Status | Evidence |
|---|---|---|---|---|
PermissionsService.cs |
SharePointPaginationHelper.GetAllItemsAsync |
Folder enumeration | WIRED | Line 222: await foreach (var item in SharePointPaginationHelper.GetAllItemsAsync(...)) |
PermissionsService.cs |
ExecuteQueryRetryHelper.ExecuteQueryRetryAsync |
All CSOM round-trips | WIRED | 7 call sites in service (lines 52, 86, 125, 217, 245, 283) |
PermissionsService.cs |
PermissionEntryHelper.IsExternalUser |
User classification | WIRED | Line 314 |
PermissionsService.cs |
PermissionEntryHelper.FilterPermissionLevels |
Level filtering | WIRED | Line 304 |
PermissionsService.cs |
PermissionEntryHelper.IsSharingLinksGroup |
Group skipping | WIRED | Line 299 |
SiteListService.cs |
SessionManager.GetOrCreateContextAsync |
Admin context acquisition | WIRED | Line 41 |
SiteListService.cs |
Microsoft.Online.SharePoint.TenantAdministration.Tenant |
GetSitePropertiesFromSharePoint |
WIRED | Line 49: new Tenant(adminCtx) |
PermissionsViewModel.cs |
IPermissionsService.ScanSiteAsync |
RunOperationAsync loop | WIRED | Line 189 |
PermissionsViewModel.cs |
CsvExportService.WriteAsync |
ExportCsvCommand handler | WIRED | Line 252 |
PermissionsViewModel.cs |
HtmlExportService.WriteAsync |
ExportHtmlCommand handler | WIRED | Line 275 |
SitePickerDialog.xaml.cs |
ISiteListService.GetSitesAsync |
Window.Loaded handler | WIRED | Line 42 |
PermissionsView.xaml.cs |
PermissionsViewModel |
GetRequiredService<PermissionsViewModel>() |
WIRED | Line 14 |
PermissionsView.xaml.cs |
SitePickerDialog |
OpenSitePickerDialog factory |
WIRED | Lines 16-19 |
App.xaml.cs |
Phase 2 services | AddTransient<IPermissionsService, PermissionsService>() etc. |
WIRED | Lines 92-100 |
Requirements Coverage
| Requirement | Source Plans | Description | Status | Evidence |
|---|---|---|---|---|
| PERM-01 | 02-01, 02-02, 02-05, 02-06, 02-07 | Single-site scan with configurable depth | SATISFIED | PermissionsService.ScanSiteAsync; SiteUrl XAML binding; ScanOptions wired |
| PERM-02 | 02-01, 02-03, 02-06, 02-07 | Multi-site scan | SATISFIED | SiteListService; SitePickerDialog; loop in RunOperationAsync; test passes |
| PERM-03 | 02-01, 02-02, 02-07 | Owners, members, guests, external users, broken inheritance | SATISFIED | Site collection admins path; #EXT# detection; PrincipalType assignment; 7 classification tests pass |
| PERM-04 | 02-01, 02-02, 02-05, 02-06, 02-07 | Include/exclude inherited permissions | SATISFIED | IncludeInherited checkbox bound; ScanOptions record passed; ExtractPermissionsAsync gate |
| PERM-05 | 02-01, 02-04, 02-07 | CSV export | SATISFIED | CsvExportService with merge, RFC 4180 escaping, UTF-8 BOM; 3 tests pass; ExportCsvCommand wired |
| PERM-06 | 02-01, 02-04, 02-07 | HTML export | SATISFIED | HtmlExportService self-contained HTML; inline CSS/JS; stats cards; external-user pills; 3 tests pass; ExportHtmlCommand wired |
| PERM-07 | 02-02, 02-07 | 5,000-item list view threshold — pagination | SATISFIED | SharePointPaginationHelper.GetAllItemsAsync called in GetFolderPermissionsAsync; RowLimit 500 CAML |
All 7 PERM requirements: SATISFIED
Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|---|---|---|---|---|
PermissionsView.xaml |
80, 84 | Hardcoded Content="Export CSV" and Content="Export HTML" instead of localization bindings |
Info | French locale users see English button labels; rad.csv.perms and rad.html.perms keys exist in resx and Designer.cs but are unused in XAML |
Strings.Designer.cs |
n/a | Missing tab_permissions static property (key exists in resx) |
Info | No functional impact — TranslationSource.Instance["tab.permissions"] resolves correctly at runtime via ResourceManager; Designer.cs property is just a typed convenience accessor |
No Blocker or Warning severity anti-patterns found.
Human Verification Required
1. Full UI visual checkpoint
Test: Run the application (dotnet run --project SharepointToolbox or F5). Navigate to the Permissions tab.
Expected:
- Tab is labelled "Permissions" (or "Permissions" in French) and shows scan options panel + empty DataGrid, not "Coming soon"
- Scan Options panel shows: Site URL input, "View Sites" button, "Scan Folders" checkbox, "Include Inherited Permissions" checkbox, "Recursive (subsites)" checkbox, "Folder depth" input, "Maximum (all levels)" checkbox
- "Generate Report" and "Cancel" buttons present
- "Export CSV" and "Export HTML" buttons are disabled (grayed out) with no results
- Click "View Sites" — SitePickerDialog opens (auth error expected if not connected — must not crash)
- Switch to French (Settings tab) — all labels in Permissions tab change to French text Why human: Visual appearance, disabled-state behavior, and locale rendering cannot be verified programmatically.
2. Export button localization decision
Test: In the running application (French locale), check the text on the Export buttons.
Expected: Either the buttons read "CSV" / "HTML" (acceptable if intentional) or the team decides to bind them to rad.csv.perms / rad.html.perms.
Why human: The XAML has Content="Export CSV" and Content="Export HTML" hardcoded — the localization keys exist but are not used. This is a minor i18n gap requiring a team decision, not a blocker.
Test Results
| Test class | Tests | Passed | Skipped | Failed |
|---|---|---|---|---|
PermissionEntryClassificationTests |
7 | 7 | 0 | 0 |
CsvExportServiceTests |
3 | 3 | 0 | 0 |
HtmlExportServiceTests |
3 | 3 | 0 | 0 |
PermissionsViewModelTests |
1 | 1 | 0 | 0 |
SiteListServiceTests |
2 | 2 | 0 | 0 |
PermissionsServiceTests |
2 | 0 | 2 | 0 |
| Full suite | 63 | 60 | 3 | 0 |
Skipped tests are intentional live-CSOM stubs (require a real SharePoint context).
Gaps Summary
No gaps blocking goal achievement. All 7 PERM requirements are implemented with real, substantive code. All key links are wired. All critical service chains are verified.
Two minor informational items were found:
- Export buttons in
PermissionsView.xamluse hardcoded English strings instead of the localization keys that exist in the resx files. This causes the buttons to stay in English when switching to French. The keysrad.csv.perms("CSV") andrad.html.perms("HTML") do exist and resolve correctly — they just aren't bound. This is a cosmetic i18n gap, not a functional failure. Strings.Designer.csis missing thetab_permissionstyped property (the key exists in both resx files and the MainWindow binding resolves it correctly at runtime viaTranslationSource).
Verified: 2026-04-02T14:30:00Z Verifier: Claude (gsd-verifier)