Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-VERIFICATION.md
2026-04-07 13:45:08 +02:00

19 KiB

phase, verified, status, score, re_verification, previous_status, previous_score, gaps_closed, gaps_remaining, regressions, human_verification
phase verified status score re_verification previous_status previous_score gaps_closed gaps_remaining regressions human_verification
07-user-access-audit 2026-04-07T12:00:00Z human_needed 25/25 must-haves verified true gaps_found 19/23 must-haves verified
Gap 1: External users now show orange 'Guest' pill badge in User column (IsExternalUser DataTrigger on Border visibility)
Gap 2: ObjectType column added between Object and Permission Level (DataGridTextColumn bound to ObjectType)
Gap 3: Test 9 SearchQuery_debounced_calls_SearchUsersAsync added — sets SearchQuery, awaits 600ms, verifies SearchUsersAsync called once
test expected why_human
Run the User Access Audit tab end-to-end with a real SharePoint tenant Typing a name shows autocomplete results from Graph API, selecting users and sites then clicking Run fills the DataGrid with color-coded rows, Export CSV and Export HTML produce valid files Requires live Graph API credentials and a SharePoint environment; cannot verify network calls or file dialogs programmatically
test expected why_human
Verify guest badge renders for external users Rows where IsExternalUser=true show an orange 'Guest' pill badge next to the login in the User column; rows where IsExternalUser=false show no badge DataTrigger-driven Visibility=Collapsed/Visible behavior requires runtime rendering to observe
test expected why_human
Verify warning icon renders for high-privilege entries Rows where IsHighPrivilege=true show a red ⚠ icon to the left of the permission level text; normal rows show no icon DataTrigger-driven visibility requires runtime rendering to observe
test expected why_human
Verify ObjectType column shows correct values ObjectType column displays meaningful values such as Site Collection, Site, List, or Folder depending on the audited object Requires live scan results to confirm service produces non-empty ObjectType values
test expected why_human
Verify group-by toggle switches grouping between user and site Clicking the ToggleButton changes DataGrid grouping header from UserLogin groups to SiteUrl groups WPF CollectionViewSource group behavior requires runtime UI to observe

Phase 7: User Access Audit — Re-Verification Report

Phase Goal: Administrators can audit every permission a specific user holds across selected sites, distinguish access types (direct/group/inherited), and export results to CSV or HTML. Verified: 2026-04-07 Status: human_needed Re-verification: Yes — after gap closure by plans 07-09 and 07-10


Re-Verification Summary

Item Previous Now Change
Guest badge for external users FAILED VERIFIED Gap closed by 07-09
ObjectType column in DataGrid FAILED VERIFIED Gap closed by 07-09
Debounced search unit test FAILED VERIFIED Gap closed by 07-10
All other truths VERIFIED VERIFIED No regressions

All 3 gaps closed. No regressions detected in DI registrations, MainWindow wiring, or previously-passing tests.


Goal Achievement

Observable Truths

# Truth Status Evidence
1 UserAccessEntry record exists with all fields needed for audit results display and export VERIFIED UserAccessEntry.cs — 12-field record with AccessType enum (Direct/Group/Inherited), IsHighPrivilege, IsExternalUser
2 IUserAccessAuditService interface defines the contract for scanning permissions filtered by user VERIFIED IUserAccessAuditService.csAuditUsersAsync with session, logins, sites, options, progress, CT
3 IGraphUserSearchService interface defines the contract for Graph API people-picker autocomplete VERIFIED IGraphUserSearchService.csSearchUsersAsync + GraphUserResult record
4 AccessType enum distinguishes Direct, Group, and Inherited access VERIFIED UserAccessEntry.cs AccessType enum (Direct/Group/Inherited)
5 UserAccessAuditService scans permissions via PermissionsService.ScanSiteAsync and filters by target user logins VERIFIED UserAccessAuditService.cs calls _permissionsService.ScanSiteAsync per site, then TransformEntries with normalized login matching
6 Each PermissionEntry is correctly classified as Direct, Group, or Inherited AccessType VERIFIED ClassifyAccessType: Inherited if !HasUniquePermissions, Group if GrantedThrough.StartsWith("SharePoint Group:"), else Direct
7 High-privilege entries (Full Control, Site Collection Administrator) are flagged VERIFIED HighPrivilegeLevels HashSet; IsHighPrivilege = HighPrivilegeLevels.Contains(trimmedLevel)
8 External users (#EXT#) are detected via PermissionEntryHelper.IsExternalUser VERIFIED PermissionEntryHelper.IsExternalUser(login) called in TransformEntries
9 Multi-user semicolon-delimited PermissionEntry rows are correctly split into per-user UserAccessEntry rows VERIFIED TransformEntries splits UserLogins and Users on ;, emits one entry per user per permission level
10 GraphUserSearchService queries Microsoft Graph /users endpoint with $filter for displayName/mail startsWith VERIFIED GraphUserSearchService.cs: startsWith(displayName,...) filter with ConsistencyLevel: eventual
11 Service returns GraphUserResult records with DisplayName, UPN, and Mail VERIFIED Maps to new GraphUserResult(DisplayName, UserPrincipalName, Mail)
12 Service handles empty queries and returns empty list VERIFIED Returns Array.Empty<>() when query is null/whitespace or length < 2
13 Service uses existing GraphClientFactory for authentication VERIFIED Constructor-injected GraphClientFactory, calls CreateClientAsync(clientId, ct)
14 ViewModel extends FeatureViewModelBase with RunOperationAsync that calls IUserAccessAuditService.AuditUsersAsync VERIFIED UserAccessAuditViewModel : FeatureViewModelBase, RunOperationAsync calls _auditService.AuditUsersAsync
15 People picker search is debounced (300ms) and calls IGraphUserSearchService.SearchUsersAsync VERIFIED DebounceSearchAsync with Task.Delay(300) calls SearchUsersAsync; covered by Test 9
16 Selected users are stored in an ObservableCollection VERIFIED ObservableCollection<GraphUserResult> _selectedUsers in ViewModel
17 Results are ObservableCollection with CollectionViewSource for grouping toggle VERIFIED _results: ObservableCollection<UserAccessEntry>, CollectionViewSource in constructor, ApplyGrouping swaps group descriptor
18 CSV export produces one file per audited user with summary section at top and flat data rows VERIFIED UserAccessCsvExportService.BuildCsv and WriteAsync group by UserLogin, emit summary then data rows
19 CSV filenames include user email and date VERIFIED $"audit_{safeLogin}_{dateStr}.csv" with dateStr = yyyy-MM-dd
20 HTML export produces a single self-contained report with collapsible groups, sortable columns, search filter VERIFIED UserAccessHtmlExportService.BuildHtml — inline CSS/JS, toggleGroup, sortTable, filterTable functions
21 HTML report has both group-by-user and group-by-site views togglable VERIFIED view-user and view-site div sections, toggleView('user'/'site') JS function
22 User Access Audit tab appears in MainWindow TabControl and is wired to DI-resolved view VERIFIED MainWindow.xaml<TabItem x:Name="UserAccessAuditTabItem">, code-behind wires content and site picker factory
23 All new services registered in DI VERIFIED App.xaml.cs lines 155-160: all six registrations present
24 High-privilege entries show warning icon (⚠) and external users show guest badge in DataGrid VERIFIED UserAccessAuditView.xaml line 231: DataTrigger Binding="{Binding IsExternalUser}" Value="True" on Border; line 256: DataTrigger Binding="{Binding IsHighPrivilege}" Value="True" on TextBlock Text="⚠"
25 ViewModel tests verify: debounced search triggers service, run audit populates results, tenant switch resets state, global sites override pattern VERIFIED 9 tests in UserAccessAuditViewModelTests.cs; Test 9 (SearchQuery_debounced_calls_SearchUsersAsync) exercises the debounce path; all 8 prior tests retained with _ discards on the new graphMock tuple slot

Score: 25/25 truths verified


Required Artifacts

Artifact Expected Status Details
SharepointToolbox/Core/Models/UserAccessEntry.cs Data model for user-centric audit results VERIFIED record UserAccessEntry + AccessType enum
SharepointToolbox/Services/IUserAccessAuditService.cs Service contract for user access auditing VERIFIED interface IUserAccessAuditService with AuditUsersAsync
SharepointToolbox/Services/IGraphUserSearchService.cs Service contract for Graph API user search VERIFIED interface IGraphUserSearchService + GraphUserResult record
SharepointToolbox/Services/UserAccessAuditService.cs Implementation of IUserAccessAuditService VERIFIED Full implementation with TransformEntries and ClassifyAccessType
SharepointToolbox/Services/GraphUserSearchService.cs Implementation of IGraphUserSearchService VERIFIED Real Graph API call with $filter
SharepointToolbox/Services/Export/UserAccessCsvExportService.cs CSV export for user access audit results VERIFIED BuildCsv, WriteAsync, WriteSingleFileAsync
SharepointToolbox/Services/Export/UserAccessHtmlExportService.cs HTML export for user access audit results VERIFIED Full self-contained HTML with dual-view, inline JS/CSS
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs Tab ViewModel for User Access Audit VERIFIED class UserAccessAuditViewModel : FeatureViewModelBase
SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml XAML layout for User Access Audit tab VERIFIED All 7 DataGrid columns present: User (with guest badge), Site, Object, Object Type, Permission Level (with ⚠ icon), Access Type, Granted Through
SharepointToolbox/Views/Tabs/UserAccessAuditView.xaml.cs Code-behind VERIFIED DI constructor wiring
SharepointToolbox/MainWindow.xaml New TabItem for User Access Audit VERIFIED UserAccessAuditTabItem present
SharepointToolbox/MainWindow.xaml.cs DI wiring for audit tab content and dialog factory VERIFIED Lines 51-62, site picker factory wired
SharepointToolbox/App.xaml.cs DI registrations for all Phase 7 services VERIFIED Lines 155-160, all 6 registrations present
SharepointToolbox/Localization/Strings.resx English localization keys for audit tab VERIFIED tab.userAccessAudit + all audit.* keys present
SharepointToolbox/Localization/Strings.fr.resx French localization keys for audit tab VERIFIED All audit.* keys present in French file
SharepointToolbox.Tests/Services/UserAccessAuditServiceTests.cs Unit tests for audit service business logic VERIFIED 12 tests: filtering, claim matching, access type classification, high-privilege, external user, semicolon splitting, multi-site
SharepointToolbox.Tests/Services/Export/UserAccessCsvExportServiceTests.cs Unit tests for CSV export VERIFIED Summary section, header, RFC 4180 escaping, column count, multi-user
SharepointToolbox.Tests/Services/Export/UserAccessHtmlExportServiceTests.cs Unit tests for HTML export VERIFIED DOCTYPE, stats cards, both view sections, access type badges, filter script
SharepointToolbox.Tests/ViewModels/UserAccessAuditViewModelTests.cs Unit tests for ViewModel logic VERIFIED 9 tests — all prior 8 retained plus Test 9 covering the debounce path

From To Via Status Details
UserAccessAuditService.cs IPermissionsService.cs Constructor injection + ScanSiteAsync call WIRED _permissionsService.ScanSiteAsync(ctx, options, progress, ct)
UserAccessAuditService.cs PermissionEntryHelper.cs IsExternalUser for guest detection WIRED PermissionEntryHelper.IsExternalUser(login) in TransformEntries
GraphUserSearchService.cs GraphClientFactory.cs Constructor injection, CreateClientAsync call WIRED _graphClientFactory.CreateClientAsync(clientId, ct)
UserAccessAuditViewModel.cs IUserAccessAuditService.cs Constructor injection, AuditUsersAsync in RunOperationAsync WIRED _auditService.AuditUsersAsync(...)
UserAccessAuditViewModel.cs IGraphUserSearchService.cs Constructor injection, SearchUsersAsync in debounced search WIRED _graphUserSearchService.SearchUsersAsync(clientId, query, 10, ct)
UserAccessAuditViewModel.cs FeatureViewModelBase.cs Extends base class WIRED class UserAccessAuditViewModel : FeatureViewModelBase
UserAccessAuditView.xaml UserAccessAuditViewModel.cs DataContext binding WIRED Constructor DataContext = viewModel, bindings on RunCommand, ExportCsvCommand, ResultsView, etc.
UserAccessAuditView.xaml UserAccessEntry.cs DataGrid column bindings WIRED Bindings on IsExternalUser, IsHighPrivilege, ObjectType, UserLogin, SiteTitle, ObjectTitle, PermissionLevel, AccessType, GrantedThrough
App.xaml.cs UserAccessAuditService.cs DI registration WIRED AddTransient<IUserAccessAuditService, UserAccessAuditService>() at line 155
UserAccessCsvExportService.cs UserAccessEntry.cs Takes IReadOnlyList<UserAccessEntry> WIRED BuildCsv(string, string, IReadOnlyList<UserAccessEntry>)
UserAccessHtmlExportService.cs UserAccessEntry.cs Takes IReadOnlyList<UserAccessEntry> WIRED BuildHtml(IReadOnlyList<UserAccessEntry>)
UserAccessAuditViewModelTests.cs IGraphUserSearchService.cs Test 9 calls SearchUsersAsync via mock WIRED graphMock.Verify(s => s.SearchUsersAsync("Ali", ...)) in Test 9

Requirements Coverage

Requirement Source Plans Description Status Evidence
UACC-01 01, 02, 03, 04, 05, 07, 08, 09, 10 User can export all SharePoint/Teams accesses a specific user has across selected sites SATISFIED Full pipeline: people-picker (GraphUserSearchService) → site selection → AuditUsersAsync scan → DataGrid display (with ObjectType column) → CSV/HTML export commands
UACC-02 01, 02, 04, 05, 06, 08, 09 Export includes direct assignments, group memberships, and inherited access SATISFIED ClassifyAccessType produces Direct/Group/Inherited; both CSV and HTML exports include "Access Type" column; DataGrid shows color-coded access types with guest badge for external users

Anti-Patterns Found

No blocker or warning anti-patterns found in the files modified by plans 07-09 and 07-10. The XAML additions use standard WPF DataTrigger patterns for conditional visibility. The test additions follow the established Moq + xUnit pattern of the existing suite.


Human Verification Required

1. End-to-End Audit Flow

Test: Connect to a real SharePoint tenant, type a partial name in the people-picker, add a user, select sites, click Run. Expected: Autocomplete dropdown (ListBox) populates with Graph API results. After Run, DataGrid fills with color-coded rows showing 7 columns: User, Site, Object, Object Type, Permission Level, Access Type, Granted Through. Why human: Requires live Azure AD credentials and SharePoint context; network calls and dialog interactions cannot be exercised by static analysis.

2. Guest Badge Rendering

Test: With results containing external users (#EXT# in login), inspect the User column in the DataGrid. Expected: Rows where IsExternalUser=true display an orange "Guest" pill badge to the right of the login. Rows where IsExternalUser=false show no badge (border is Collapsed). Why human: DataTrigger-driven Visibility=Collapsed/Visible behavior requires WPF runtime rendering to observe.

3. Warning Icon Rendering

Test: With results containing high-privilege entries (Full Control, Site Collection Administrator), inspect the Permission Level column. Expected: Rows where IsHighPrivilege=true show a red ⚠ icon to the left of the permission level text. Normal rows show no icon. High-privilege rows also remain bold at row level. Why human: DataTrigger-driven visibility and combined row/cell styling requires WPF runtime rendering to observe.

4. ObjectType Column Values

Test: Run an audit against a site with diverse object types (site collection root, subsite, document library, folder). Expected: The ObjectType column displays values such as "Site Collection", "Site", "List", "Folder" — not empty strings. Why human: Requires live scan data to confirm UserAccessAuditService.TransformEntries produces non-empty ObjectType values that flow through to the DataGrid.

5. Export File Quality

Test: With results loaded, click "Export CSV" and "Export HTML". Open both files. Expected: CSV has summary section at top, 7-column data rows (now including Object Type), UTF-8 BOM. HTML opens in browser with stats cards, both By-User and By-Site view toggles functional, filter input narrows rows, sortable columns work. Why human: File system dialog interaction and HTML rendering require manual inspection.

6. Group-By Toggle

Test: Click the group-by ToggleButton while the DataGrid has results. Expected: DataGrid group headers switch between UserLogin groups and SiteUrl groups in real time. Why human: WPF CollectionViewSource grouping behavior requires runtime UI to observe.


Closure Summary

All three previously-identified gaps are confirmed closed by direct inspection of the codebase:

Gap 1 — Closed: UserAccessAuditView.xaml User column (lines 220-242) is now a DataGridTemplateColumn with a StackPanel containing the UserLogin TextBlock and a Border with orange #F39C12 background and "Guest" text. The Border.Style has a DataTrigger on IsExternalUser=True that sets Visibility=Visible (collapsed by default). This matches the plan 09 specification exactly.

Gap 2 — Closed: UserAccessAuditView.xaml line 245: <DataGridTextColumn Header="Object Type" Binding="{Binding ObjectType}" Width="90" /> inserted between the Object column (line 244) and the Permission Level column (line 246). Column order is now: User, Site, Object, Object Type, Permission Level, Access Type, Granted Through (7 columns).

Gap 3 — Closed: UserAccessAuditViewModelTests.cs now has 9 [Fact] methods. Test 9 (SearchQuery_debounced_calls_SearchUsersAsync) sets a TenantSwitchedMessage profile, assigns vm.SearchQuery = "Ali", awaits 600ms, then verifies graphMock.Verify(s => s.SearchUsersAsync(..., "Ali", ...), Times.Once). The CreateViewModel helper was extended to a 3-tuple returning (vm, auditMock, graphMock); all 8 prior tests updated to use _ discards.

No regressions were found: DI registrations at App.xaml.cs lines 155-160 remain intact; MainWindow.xaml/.cs wiring unchanged; all previously-verified service and export artifacts unmodified.


Verified: 2026-04-07 Verifier: Claude (gsd-verifier)