---
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<GraphUserResult> | VERIFIED | `ObservableCollection<GraphUserResult> _selectedUsers` in ViewModel |
| 17 | Results are ObservableCollection<UserAccessEntry> 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 |

---

## 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<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)_
