diff --git a/.planning/STATE.md b/.planning/STATE.md index 8094274..3f7f8f3 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,7 +4,7 @@ milestone: v1.0 milestone_name: milestone status: completed stopped_at: Completed 07-10-PLAN.md -last_updated: "2026-04-07T11:16:11.770Z" +last_updated: "2026-04-07T11:44:59.053Z" last_activity: 2026-04-07 — Roadmap created (Phases 6-9), 10/10 requirements mapped progress: total_phases: 4 diff --git a/.planning/phases/07-user-access-audit/07-VERIFICATION.md b/.planning/phases/07-user-access-audit/07-VERIFICATION.md new file mode 100644 index 0000000..aac1c50 --- /dev/null +++ b/.planning/phases/07-user-access-audit/07-VERIFICATION.md @@ -0,0 +1,206 @@ +--- +phase: 07-user-access-audit +verified: 2026-04-07T12:00:00Z +status: human_needed +score: 25/25 must-haves verified +re_verification: true +previous_status: gaps_found +previous_score: 19/23 must-haves verified +gaps_closed: + - "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" +gaps_remaining: [] +regressions: [] +human_verification: + - test: "Run the User Access Audit tab end-to-end with a real SharePoint tenant" + expected: "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" + why_human: "Requires live Graph API credentials and a SharePoint environment; cannot verify network calls or file dialogs programmatically" + - test: "Verify guest badge renders for external users" + expected: "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" + why_human: "DataTrigger-driven Visibility=Collapsed/Visible behavior requires runtime rendering to observe" + - test: "Verify warning icon renders for high-privilege entries" + expected: "Rows where IsHighPrivilege=true show a red ⚠ icon to the left of the permission level text; normal rows show no icon" + why_human: "DataTrigger-driven visibility requires runtime rendering to observe" + - test: "Verify ObjectType column shows correct values" + expected: "ObjectType column displays meaningful values such as Site Collection, Site, List, or Folder depending on the audited object" + why_human: "Requires live scan results to confirm service produces non-empty ObjectType values" + - test: "Verify group-by toggle switches grouping between user and site" + expected: "Clicking the ToggleButton changes DataGrid grouping header from UserLogin groups to SiteUrl groups" + why_human: "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.cs` — `AuditUsersAsync` with session, logins, sites, options, progress, CT | +| 3 | IGraphUserSearchService interface defines the contract for Graph API people-picker autocomplete | VERIFIED | `IGraphUserSearchService.cs` — `SearchUsersAsync` + `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 _selectedUsers` in ViewModel | +| 17 | Results are ObservableCollection with CollectionViewSource for grouping toggle | VERIFIED | `_results: ObservableCollection`, `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` — ``, 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 | + +--- + +## Key Link Verification + +| 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()` at line 155 | +| `UserAccessCsvExportService.cs` | `UserAccessEntry.cs` | Takes `IReadOnlyList` | WIRED | `BuildCsv(string, string, IReadOnlyList)` | +| `UserAccessHtmlExportService.cs` | `UserAccessEntry.cs` | Takes `IReadOnlyList` | WIRED | `BuildHtml(IReadOnlyList)` | +| `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: `` 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)_