Files
Sharepoint-Toolbox/.planning/phases/07-user-access-audit/07-04-PLAN.md
Dev 19e4c3852d docs(07): create phase plan - 8 plans across 5 waves
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:32:39 +02:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
07-user-access-audit 04 execute 3
07-01
07-02
07-03
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs
true
UACC-01
UACC-02
truths artifacts key_links
ViewModel extends FeatureViewModelBase with RunOperationAsync that calls IUserAccessAuditService.AuditUsersAsync
People picker search is debounced (300ms) and calls IGraphUserSearchService.SearchUsersAsync
Selected users are stored in an ObservableCollection<GraphUserResult>
Results are ObservableCollection<UserAccessEntry> with CollectionViewSource for grouping toggle
ExportCsvCommand and ExportHtmlCommand follow PermissionsViewModel pattern
Site selection follows _hasLocalSiteOverride + OnGlobalSitesChanged pattern from PermissionsViewModel
Per-user summary banner properties (TotalAccesses, SitesCount, HighPrivilegeCount) are computed from results
FilterText property filters the CollectionView in real-time
path provides contains
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs Tab ViewModel for User Access Audit class UserAccessAuditViewModel
from to via pattern
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs SharepointToolbox/Services/IUserAccessAuditService.cs Constructor injection, AuditUsersAsync call in RunOperationAsync AuditUsersAsync
from to via pattern
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs SharepointToolbox/Services/IGraphUserSearchService.cs Constructor injection, SearchUsersAsync call in debounced search SearchUsersAsync
from to via pattern
SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs SharepointToolbox/ViewModels/FeatureViewModelBase.cs Extends base class, overrides RunOperationAsync and OnGlobalSitesChanged FeatureViewModelBase
Implement UserAccessAuditViewModel — the tab ViewModel that orchestrates people picker search, site selection, audit execution, result grouping/filtering, summary banner, and export commands.

Purpose: Central coordinator between UI and services. This is the largest single file in the phase, connecting all pieces. Output: UserAccessAuditViewModel.cs

<execution_context> @C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/dev/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/07-user-access-audit/07-CONTEXT.md @.planning/phases/07-user-access-audit/07-01-SUMMARY.md @.planning/phases/07-user-access-audit/07-02-SUMMARY.md @.planning/phases/07-user-access-audit/07-03-SUMMARY.md From SharepointToolbox/Core/Models/UserAccessEntry.cs: ```csharp public enum AccessType { Direct, Group, Inherited }

public record UserAccessEntry( string UserDisplayName, string UserLogin, string SiteUrl, string SiteTitle, string ObjectType, string ObjectTitle, string ObjectUrl, string PermissionLevel, AccessType AccessType, string GrantedThrough, bool IsHighPrivilege, bool IsExternalUser);


From SharepointToolbox/Services/IGraphUserSearchService.cs:
```csharp
public record GraphUserResult(string DisplayName, string UserPrincipalName, string? Mail);

public interface IGraphUserSearchService
{
    Task<IReadOnlyList<GraphUserResult>> SearchUsersAsync(
        string clientId, string query, int maxResults = 10, CancellationToken ct = default);
}

From SharepointToolbox/Services/IUserAccessAuditService.cs:

public interface IUserAccessAuditService
{
    Task<IReadOnlyList<UserAccessEntry>> AuditUsersAsync(
        ISessionManager sessionManager,
        IReadOnlyList<string> targetUserLogins,
        IReadOnlyList<SiteInfo> sites,
        ScanOptions options,
        IProgress<OperationProgress> progress,
        CancellationToken ct);
}
public abstract partial class FeatureViewModelBase : ObservableRecipient
{
    protected IReadOnlyList<SiteInfo> GlobalSites { get; private set; }
    protected abstract Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress);
    protected virtual void OnTenantSwitched(TenantProfile profile) { }
    protected virtual void OnGlobalSitesChanged(IReadOnlyList<SiteInfo> sites) { }
    // RunCommand, CancelCommand, IsRunning, StatusMessage, ProgressValue auto-provided
}

From SharepointToolbox/ViewModels/Tabs/PermissionsViewModel.cs:

public ObservableCollection<SiteInfo> SelectedSites { get; } = new();
private bool _hasLocalSiteOverride;
public Func<Window>? OpenSitePickerDialog { get; set; }
internal TenantProfile? _currentProfile;
public IAsyncRelayCommand ExportCsvCommand { get; }
public IAsyncRelayCommand ExportHtmlCommand { get; }
Task 1: Implement UserAccessAuditViewModel SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs Create `SharepointToolbox/ViewModels/Tabs/UserAccessAuditViewModel.cs`. This is a substantial file (~350 lines). Follow the PermissionsViewModel pattern exactly for site selection, tenant switching, export commands, and dialog factories.
Structure:
1. **Fields**: inject IUserAccessAuditService, IGraphUserSearchService, ISessionManager, export services, logger
2. **Observable properties**:
   - `SearchQuery` (string) — people picker text input, triggers debounced search on change
   - `SearchResults` (ObservableCollection<GraphUserResult>) — autocomplete dropdown items
   - `SelectedUsers` (ObservableCollection<GraphUserResult>) — users added for audit
   - `Results` (ObservableCollection<UserAccessEntry>) — audit output
   - `FilterText` (string) — real-time filter on results grid
   - `IsGroupByUser` (bool, default true) — toggle between group-by-user and group-by-site
   - `IncludeInherited` (bool) — scan option
   - `ScanFolders` (bool, default true) — scan option
   - `IncludeSubsites` (bool) — scan option
   - `IsSearching` (bool) — shows spinner during Graph search
3. **Summary properties** (computed, not stored):
   - `TotalAccessCount` => Results.Count
   - `SitesCount` => Results.Select(r => r.SiteUrl).Distinct().Count()
   - `HighPrivilegeCount` => Results.Count(r => r.IsHighPrivilege)
   - `SelectedUsersLabel` => e.g. "2 user(s) selected"
4. **Commands**:
   - `ExportCsvCommand` (AsyncRelayCommand, CanExport)
   - `ExportHtmlCommand` (AsyncRelayCommand, CanExport)
   - `OpenSitePickerCommand` (RelayCommand)
   - `AddUserCommand` (RelayCommand<GraphUserResult>) — adds to SelectedUsers
   - `RemoveUserCommand` (RelayCommand<GraphUserResult>) — removes from SelectedUsers
5. **Site picker**: SelectedSites, _hasLocalSiteOverride, OpenSitePickerDialog factory, SitesSelectedLabel — identical pattern to PermissionsViewModel
6. **People picker debounce**: Use a CancellationTokenSource that is cancelled/recreated each time SearchQuery changes. Delay 300ms before calling SearchUsersAsync. Set IsSearching during search.
7. **RunOperationAsync**: Build ScanOptions, call AuditUsersAsync with SelectedUsers UPNs + effective sites, update Results on UI thread, notify summary properties and export CanExecute.
8. **CollectionViewSource**: Create a ResultsView (ICollectionView) backed by Results. When IsGroupByUser changes, update GroupDescriptions (group by UserLogin or SiteUrl). When FilterText changes, apply filter predicate.
9. **Constructors**: Full DI constructor + internal test constructor (omit export services) — same dual-constructor pattern as PermissionsViewModel.
10. **Tenant switching**: Reset all state (results, selected users, search, sites) in OnTenantSwitched.

Important implementation details:
- The debounced search should use `Task.Delay(300, ct)` pattern with a field `_searchCts` that gets cancelled on each new keystroke
- partial void OnSearchQueryChanged(string value) triggers the debounced search
- partial void OnFilterTextChanged(string value) triggers ResultsView.Refresh()
- partial void OnIsGroupByUserChanged(bool value) triggers re-grouping of ResultsView
- Export CSV/HTML: use SaveFileDialog pattern from PermissionsViewModel, calling the audit-specific export services (UserAccessCsvExportService, UserAccessHtmlExportService) that will be created in plan 07-06
- Export services are typed as object references (UserAccessCsvExportService? and UserAccessHtmlExportService?) since they haven't been created yet — the plan 07-06 export service files will be the concrete types
- For the test constructor, pass null for export services

The ViewModel needs these `using` statements:
```
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Data;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Services;
using SharepointToolbox.Services.Export;
```
cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 UserAccessAuditViewModel.cs compiles and extends FeatureViewModelBase. It has: people picker with debounced Graph search, site selection with override pattern, RunOperationAsync calling AuditUsersAsync, Results with CollectionViewSource grouping and filtering, summary properties, dual constructors, export commands, tenant switching reset. - `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors - UserAccessAuditViewModel extends FeatureViewModelBase - Has ObservableProperty for SearchQuery, SelectedUsers, Results, FilterText, IsGroupByUser - Has ExportCsvCommand, ExportHtmlCommand, OpenSitePickerCommand, AddUserCommand, RemoveUserCommand - RunOperationAsync calls IUserAccessAuditService.AuditUsersAsync - OnSearchQueryChanged triggers debounced IGraphUserSearchService.SearchUsersAsync - ResultsView ICollectionView supports group-by toggle and text filter

<success_criteria> The ViewModel is the orchestration hub for the audit tab. All UI interactions (search users, select sites, run audit, filter results, toggle grouping, export) are wired to service calls and observable state. Ready for View binding in 07-05 and export service implementation in 07-06. </success_criteria>

After completion, create `.planning/phases/07-user-access-audit/07-04-SUMMARY.md`